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

Add rename support for constants #2626

Merged
merged 1 commit into from
Oct 2, 2024
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
Binary file added jekyll/images/rename.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions jekyll/index.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Want to discuss Ruby developer experience? Consider joining the public
- [Show syntax tree](#show-syntax-tree)
- [ERB support](#erb-support)
- [Guessed types](#guessed-types)
- [Rename symbol](#rename-symbol)
- [VS Code only features](#vs-code-features)
- [Dependencies view](#dependencies-view)
- [Rails generator integrations](#rails-generator-integrations)
Expand Down Expand Up @@ -409,6 +410,14 @@ end
# randomly
user.a
```
### Rename symbol

Rename allows developers to rename all occurrences of the entity under the cursor across the entire project. In VS Code
renaming can be triggered by right clicking the entity to rename or by pressing F2 on it. You can also preview the
edits that will be applied by pressing CTRL/CMD + Enter after typing the desired new name.

![Rename demo](images/rename.gif)

## VS Code features

The following features are all custom made for VS Code.
Expand Down
262 changes: 262 additions & 0 deletions lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# typed: strict
# frozen_string_literal: true

module RubyIndexer
class ReferenceFinder
extend T::Sig

class Reference
extend T::Sig

sig { returns(String) }
attr_reader :name

sig { returns(Prism::Location) }
attr_reader :location

sig { params(name: String, location: Prism::Location).void }
def initialize(name, location)
@name = name
@location = location
end
end

sig { returns(T::Array[Reference]) }
attr_reader :references

sig do
params(
fully_qualified_name: String,
index: RubyIndexer::Index,
dispatcher: Prism::Dispatcher,
).void
end
def initialize(fully_qualified_name, index, dispatcher)
@fully_qualified_name = fully_qualified_name
@index = index
@stack = T.let([], T::Array[String])
@references = T.let([], T::Array[Reference])

dispatcher.register(
self,
:on_class_node_enter,
:on_class_node_leave,
:on_module_node_enter,
:on_module_node_leave,
:on_singleton_class_node_enter,
:on_singleton_class_node_leave,
:on_def_node_enter,
:on_def_node_leave,
:on_multi_write_node_enter,
:on_constant_path_write_node_enter,
:on_constant_path_or_write_node_enter,
:on_constant_path_operator_write_node_enter,
:on_constant_path_and_write_node_enter,
:on_constant_or_write_node_enter,
:on_constant_path_node_enter,
:on_constant_read_node_enter,
:on_constant_write_node_enter,
:on_constant_or_write_node_enter,
:on_constant_and_write_node_enter,
:on_constant_operator_write_node_enter,
)
end

sig { params(node: Prism::ClassNode).void }
def on_class_node_enter(node)
constant_path = node.constant_path
name = constant_path.slice
nesting = actual_nesting(name)

if nesting.join("::") == @fully_qualified_name
@references << Reference.new(name, constant_path.location)
end

@stack << name
end

sig { params(node: Prism::ClassNode).void }
def on_class_node_leave(node)
@stack.pop
end

sig { params(node: Prism::ModuleNode).void }
def on_module_node_enter(node)
constant_path = node.constant_path
name = constant_path.slice
nesting = actual_nesting(name)

if nesting.join("::") == @fully_qualified_name
@references << Reference.new(name, constant_path.location)
end

@stack << name
end

sig { params(node: Prism::ModuleNode).void }
def on_module_node_leave(node)
@stack.pop
end

sig { params(node: Prism::SingletonClassNode).void }
def on_singleton_class_node_enter(node)
expression = node.expression
return unless expression.is_a?(Prism::SelfNode)

@stack << "<Class:#{@stack.last}>"
end

sig { params(node: Prism::SingletonClassNode).void }
def on_singleton_class_node_leave(node)
@stack.pop
end

sig { params(node: Prism::ConstantPathNode).void }
def on_constant_path_node_enter(node)
name = constant_name(node)
return unless name

collect_constant_references(name, node.location)
end

sig { params(node: Prism::ConstantReadNode).void }
def on_constant_read_node_enter(node)
name = constant_name(node)
return unless name

collect_constant_references(name, node.location)
end

sig { params(node: Prism::MultiWriteNode).void }
def on_multi_write_node_enter(node)
[*node.lefts, *node.rest, *node.rights].each do |target|
case target
when Prism::ConstantTargetNode, Prism::ConstantPathTargetNode
collect_constant_references(target.name.to_s, target.location)
end
end
end

sig { params(node: Prism::ConstantPathWriteNode).void }
def on_constant_path_write_node_enter(node)
target = node.target
return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)

name = constant_name(target)
return unless name

collect_constant_references(name, target.location)
end

sig { params(node: Prism::ConstantPathOrWriteNode).void }
def on_constant_path_or_write_node_enter(node)
target = node.target
return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)

name = constant_name(target)
return unless name

collect_constant_references(name, target.location)
end

sig { params(node: Prism::ConstantPathOperatorWriteNode).void }
def on_constant_path_operator_write_node_enter(node)
target = node.target
return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)

name = constant_name(target)
return unless name

collect_constant_references(name, target.location)
end

sig { params(node: Prism::ConstantPathAndWriteNode).void }
def on_constant_path_and_write_node_enter(node)
target = node.target
return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)

name = constant_name(target)
return unless name

collect_constant_references(name, target.location)
end

sig { params(node: Prism::ConstantWriteNode).void }
def on_constant_write_node_enter(node)
collect_constant_references(node.name.to_s, node.name_loc)
end

sig { params(node: Prism::ConstantOrWriteNode).void }
def on_constant_or_write_node_enter(node)
collect_constant_references(node.name.to_s, node.name_loc)
end

sig { params(node: Prism::ConstantAndWriteNode).void }
def on_constant_and_write_node_enter(node)
collect_constant_references(node.name.to_s, node.name_loc)
end

sig { params(node: Prism::ConstantOperatorWriteNode).void }
def on_constant_operator_write_node_enter(node)
collect_constant_references(node.name.to_s, node.name_loc)
end

sig { params(node: Prism::DefNode).void }
def on_def_node_enter(node)
if node.receiver.is_a?(Prism::SelfNode)
@stack << "<Class:#{@stack.last}>"
end
end

sig { params(node: Prism::DefNode).void }
def on_def_node_leave(node)
if node.receiver.is_a?(Prism::SelfNode)
@stack.pop
end
end

private

sig { params(name: String).returns(T::Array[String]) }
def actual_nesting(name)
vinistock marked this conversation as resolved.
Show resolved Hide resolved
nesting = @stack + [name]
corrected_nesting = []

nesting.reverse_each do |name|
corrected_nesting.prepend(name.delete_prefix("::"))

break if name.start_with?("::")
end

corrected_nesting
end

sig { params(name: String, location: Prism::Location).void }
def collect_constant_references(name, location)
entries = @index.resolve(name, @stack)
return unless entries

entries.each do |entry|
next unless entry.name == @fully_qualified_name

@references << Reference.new(name, location)
end
end

sig do
params(
node: T.any(
Prism::ConstantPathNode,
Prism::ConstantReadNode,
Prism::ConstantPathTargetNode,
),
).returns(T.nilable(String))
end
def constant_name(node)
node.full_name
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
Prism::ConstantPathNode::MissingNodesInConstantPathError
nil
end
end
end
1 change: 1 addition & 0 deletions lib/ruby_indexer/ruby_indexer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

require "ruby_indexer/lib/ruby_indexer/indexable_path"
require "ruby_indexer/lib/ruby_indexer/declaration_listener"
require "ruby_indexer/lib/ruby_indexer/reference_finder"
require "ruby_indexer/lib/ruby_indexer/enhancement"
require "ruby_indexer/lib/ruby_indexer/index"
require "ruby_indexer/lib/ruby_indexer/entry"
Expand Down
86 changes: 86 additions & 0 deletions lib/ruby_indexer/test/reference_finder_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# typed: true
# frozen_string_literal: true

require "test_helper"

module RubyIndexer
class ReferenceFinderTest < Minitest::Test
def test_finds_constant_references
vinistock marked this conversation as resolved.
Show resolved Hide resolved
refs = find_references("Foo::Bar", <<~RUBY)
module Foo
class Bar
end

Bar
end

Foo::Bar
RUBY

assert_equal("Bar", refs[0].name)
assert_equal(2, refs[0].location.start_line)

assert_equal("Bar", refs[1].name)
assert_equal(5, refs[1].location.start_line)

assert_equal("Foo::Bar", refs[2].name)
assert_equal(8, refs[2].location.start_line)
end

def test_finds_constant_references_inside_singleton_contexts
refs = find_references("Foo::<Class:Foo>::Bar", <<~RUBY)
class Foo
class << self
class Bar
end

Bar
end
end
RUBY

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

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

def test_finds_top_level_constant_references
refs = find_references("Bar", <<~RUBY)
class Bar
end

class Foo
::Bar

class << self
::Bar
end
end
RUBY

assert_equal("Bar", refs[0].name)
assert_equal(1, refs[0].location.start_line)

assert_equal("::Bar", refs[1].name)
assert_equal(5, refs[1].location.start_line)

assert_equal("::Bar", refs[2].name)
assert_equal(8, refs[2].location.start_line)
end

private

def find_references(fully_qualified_name, source)
file_path = "/fake.rb"
index = Index.new
index.index_single(IndexablePath.new(nil, file_path), source)
parse_result = Prism.parse(source)
dispatcher = Prism::Dispatcher.new
finder = ReferenceFinder.new(fully_qualified_name, index, dispatcher)
dispatcher.visit(parse_result.value)
finder.references.uniq(&:location)
end
end
end
Loading
Loading