Skip to content

Add hover documentation for reserved keywords #3549

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

Closed
Closed
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
13 changes: 10 additions & 3 deletions lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,14 @@ class InstanceVariableTarget < Target
#: String
attr_reader :name

#: (String name) -> void
def initialize(name)
#: Array[String]
attr_reader :owner_ancestors

#: (String name, Array[String] owner_ancestors) -> void
def initialize(name, owner_ancestors)
super()
@name = name
@owner_ancestors = owner_ancestors
end
end

Expand Down Expand Up @@ -322,7 +326,10 @@ def collect_constant_references(name, location)
def collect_instance_variable_references(name, location, declaration)
return unless @target.is_a?(InstanceVariableTarget) && name == @target.name

@references << Reference.new(name, location, declaration: declaration)
receiver_type = Index.actual_nesting(@stack, nil).join("::")
if @target.owner_ancestors.include?(receiver_type)
@references << Reference.new(name, location, declaration: declaration)
end
end
end
end
93 changes: 79 additions & 14 deletions lib/ruby_indexer/test/reference_finder_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -216,22 +216,43 @@ def foo
assert_equal(11, refs[2].location.start_line)
end

def test_finds_instance_variable_read_references
refs = find_instance_variable_references("@foo", <<~RUBY)
def test_finds_instance_variable_references
refs = find_instance_variable_references("@name", ["Foo"], <<~RUBY)
class Foo
def foo
@foo
def initialize
@name = "foo"
end
def name
@name
end
def name_capital
@name[0]
end
end

class Bar
def initialize
@name = "foo"
end
def name
@name
end
end
RUBY
assert_equal(1, refs.size)
assert_equal(3, refs.size)

assert_equal("@foo", refs[0].name)
assert_equal("@name", refs[0].name)
assert_equal(3, refs[0].location.start_line)

assert_equal("@name", refs[1].name)
assert_equal(6, refs[1].location.start_line)

assert_equal("@name", refs[2].name)
assert_equal(9, refs[2].location.start_line)
end

def test_finds_instance_variable_write_references
refs = find_instance_variable_references("@foo", <<~RUBY)
refs = find_instance_variable_references("@foo", ["Foo"], <<~RUBY)
class Foo
def write
@foo = 1
Expand All @@ -252,26 +273,70 @@ def write
assert_equal(7, refs[4].location.start_line)
end

def test_finds_instance_variable_references_ignore_context
refs = find_instance_variable_references("@name", <<~RUBY)
class Foo
def test_finds_instance_variable_references_in_owner_ancestors
refs = find_instance_variable_references("@name", ["Foo", "Base", "Top", "Parent"], <<~RUBY)
module Base
def change_name(name)
@name = name
end
def name
@name
end

module ::Top
def name
@name
end
end
end

class Parent
def initialize
@name = "parent"
end
def name_capital
@name[0]
end
end

class Foo < Parent
include Base
def initialize
@name = "foo"
end
def name
@name
end
end

class Bar
def name
@name = "bar"
end
end
RUBY
assert_equal(2, refs.size)
assert_equal(7, refs.size)

assert_equal("@name", refs[0].name)
assert_equal(3, refs[0].location.start_line)

assert_equal("@name", refs[1].name)
assert_equal(8, refs[1].location.start_line)
assert_equal(6, refs[1].location.start_line)

assert_equal("@name", refs[2].name)
assert_equal(11, refs[2].location.start_line)

assert_equal("@name", refs[3].name)
assert_equal(18, refs[3].location.start_line)

assert_equal("@name", refs[4].name)
assert_equal(21, refs[4].location.start_line)

assert_equal("@name", refs[5].name)
assert_equal(28, refs[5].location.start_line)

assert_equal("@name", refs[6].name)
assert_equal(31, refs[6].location.start_line)
end

def test_accounts_for_reopened_classes
Expand Down Expand Up @@ -310,8 +375,8 @@ def find_method_references(method_name, source)
find_references(target, source)
end

def find_instance_variable_references(instance_variable_name, source)
target = ReferenceFinder::InstanceVariableTarget.new(instance_variable_name)
def find_instance_variable_references(instance_variable_name, owner_ancestors, source)
target = ReferenceFinder::InstanceVariableTarget.new(instance_variable_name, owner_ancestors)
find_references(target, source)
end

Expand Down
9 changes: 9 additions & 0 deletions lib/ruby_lsp/listeners/hover.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ class Hover
include Requests::Support::Common

ALLOWED_TARGETS = [
Prism::BeginNode,
Prism::BreakNode,
Prism::CaseNode,
Prism::CallNode,
Prism::ConstantReadNode,
Prism::ConstantWriteNode,
Expand Down Expand Up @@ -54,6 +57,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so

dispatcher.register(
self,
:on_break_node_enter,
:on_constant_read_node_enter,
:on_constant_write_node_enter,
:on_constant_path_node_enter,
Expand Down Expand Up @@ -244,6 +248,11 @@ def on_class_variable_write_node_enter(node)
handle_class_variable_hover(node.name.to_s)
end

#: (Prism::BreakNode node) -> void
def on_break_node_enter(node)
handle_keyword_documentation(node.keyword)
end

private

#: ((Prism::InterpolatedStringNode | Prism::StringNode) node) -> void
Expand Down
6 changes: 5 additions & 1 deletion lib/ruby_lsp/requests/references.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@ def create_reference_target(target_node, node_context)
Prism::InstanceVariableReadNode,
Prism::InstanceVariableTargetNode,
Prism::InstanceVariableWriteNode
RubyIndexer::ReferenceFinder::InstanceVariableTarget.new(target_node.name.to_s)
receiver_type = @global_state.type_inferrer.infer_receiver_type(node_context)
return unless receiver_type

ancestors = @global_state.index.linearized_ancestors_of(receiver_type.name)
RubyIndexer::ReferenceFinder::InstanceVariableTarget.new(target_node.name.to_s, ancestors)
when Prism::CallNode, Prism::DefNode
RubyIndexer::ReferenceFinder::MethodTarget.new(target_node.name.to_s)
end
Expand Down
3 changes: 3 additions & 0 deletions lib/ruby_lsp/static_docs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ module RubyLsp
# A map of keyword => short documentation to be displayed on hover or completion
KEYWORD_DOCS = {
"yield" => "Invokes the passed block with the given arguments",
"case" => "Starts a case expression for pattern matching or multiple condition checking",
"begin" => "Starts an exception handling block or ensures code is executed in order",
"break" => "Terminates the execution of a block, loop, or method",
}.freeze #: Hash[String, String]
end
132 changes: 132 additions & 0 deletions static_docs/alias.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Alias

In Ruby, the `alias` keyword creates an alternative name for an existing method or constant. This allows you to call the same method using different names, which is particularly useful for method deprecation, creating shortcuts, or improving code readability.

```ruby
# Basic method aliasing
class User
def full_name
"#{first_name} #{last_name}"
end

alias name full_name
end

user = User.new
user.full_name # => "John Smith"
user.name # => "John Smith"
```

When you create an alias, it creates a copy of the method at the time the alias is defined. This means that if you later modify the original method, the alias will still use the old version.

```ruby
class Calculator
def add(a, b)
a + b
end

alias plus add

# Modifying the original method
def add(a, b)
puts "Adding #{a} and #{b}"
a + b
end
end

calc = Calculator.new
calc.plus(2, 3) # => 5 (no output)
calc.add(2, 3) # Prints "Adding 2 and 3" then returns 5
```

## Using alias_method

Ruby also provides `alias_method`, which is more flexible as it can accept dynamic method names and is more commonly used in modern Ruby:

```ruby
class Service
def process_data
puts "Processing..."
end

# Using alias_method with symbols
alias_method :execute, :process_data

# Can also use strings
alias_method "run", "process_data"
end

service = Service.new
service.process_data # => "Processing..."
service.execute # => "Processing..."
service.run # => "Processing..."
```

## Common Use Cases

### Method Deprecation

```ruby
class API
def fetch_users
# New implementation
User.all.includes(:preferences)
end

alias get_users fetch_users

def get_users
warn "[DEPRECATED] `get_users` is deprecated. Please use `fetch_users` instead"
fetch_users
end
end
```

### Creating Shorter Names

```ruby
class StringUtils
def self.convert_to_uppercase(text)
text.upcase
end

class << self
alias up convert_to_uppercase
end
end

StringUtils.convert_to_uppercase("hello") # => "HELLO"
StringUtils.up("hello") # => "HELLO"
```

### Aliasing Operators

```ruby
class Vector
def initialize(x, y)
@x = x
@y = y
end

def add(other)
Vector.new(@x + other.x, @y + other.y)
end

# Make + work the same as add
alias + add

protected

attr_reader :x, :y
end

v1 = Vector.new(1, 2)
v2 = Vector.new(3, 4)
v3 = v1 + v2 # Same as v1.add(v2)
```

Remember that while aliasing can be useful, it should be used judiciously. Too many aliases can make code harder to understand and maintain. It's best used for:
- Creating more intuitive method names
- Supporting backward compatibility
- Implementing operator overloading
- Creating shortcuts for frequently used methods
Loading
Loading