Skip to content

Add references, rename and go to definition for local variables - #4260

Open
Mohith26 wants to merge 1 commit into
Shopify:mainfrom
Mohith26:local-variable-references
Open

Mohith26 wants to merge 1 commit into
Shopify:mainfrom
Mohith26:local-variable-references

Conversation

@Mohith26

@Mohith26 Mohith26 commented Nov 4, 2026 •

Copy link
Copy Markdown

Motivation

Closes #3111
Closes #2642

Find references and rename return nothing for local variables, and go to definition says "No definition found". This is one of the most common things people reach for in an editor, and the earlier attempt (#3558) went stale.

Implementation

  • Requests::Support::LocalVariableFinder is a Prism::Visitor that groups every occurrence of a local by the scope that declares it, using the depth Prism stores on local variable nodes. Def, class, module and singleton class bodies are hard boundaries; blocks and lambdas can see their parents. Superclasses, class << expr and def recv.m receivers are visited in the outer scope, as Ruby evaluates them.
  • Covered: reads, all write forms, multi-assign, for and rescue => e targets, every parameter kind (including destructured block params), block locals (|a; b|), pattern matching (^pin, => var, in { name: }), regexp named captures (the name inside the group is used), and { name: } / foo(name:) shorthands. Numbered params and it are ignored.
  • References, prepare rename and rename use it first when the cursor is on a local. Only the current document is searched since locals can't leak across files, so there is no workspace glob for these requests. includeDeclaration: false excludes the declaration.
  • Rename:
    • validates the new name by parsing <name> = nil with Prism (rejects constants, keywords, _1, etc.)
    • raises InvalidNameError if the new name is a local or a bare method call in any scope that can see the variable, since that would change behavior
    • keeps the key for shorthands: { name: } becomes { name: new_name }
    • refuses keyword parameters (prepare rename returns nil, rename raises), since callers pass them by name
  • Definition jumps to the parameter or first assignment. It's skipped in typed: true or higher files where Sorbet already provides this. test_definition_call_node_precision asserted that the foo receiver in foo.message returns nothing; it now jumps to the foo parameter, so I updated that assertion.
  • Ranges use the negotiated position encoding, so names after multibyte characters land correctly.
  • Docs for rename, references and definition are updated.

The finder doesn't touch the indexer, so porting this onto the Rubydex branch should only mean adjusting the few lines at the top of each request. Happy to do that if you'd rather it land there.

Building the finder for a 1,500 line file (lib/ruby_lsp/server.rb) takes about 2 ms on my machine (Apple Silicon, median of 15 runs).

Automated Tests

Added test/requests/support/local_variable_finder_test.rb (scoping, shadowing, every parameter type, patterns, regexp captures, shorthands, cursor at the end of a name, multibyte, name collision checks) plus request-level tests for references (including ERB and includeDeclaration: false), rename (methods, params, shorthands, hash patterns, regexp captures, multibyte, collisions, invalid names, keyword params), prepare rename and definition (including the Sorbet case).

Ran bundle exec rake test, Sorbet type checking (the sorbet binary directly, since srb tc's cache couldn't run in my sandbox) and bundle exec rubocop locally. Everything related passes; the only failures I saw locally are in setup_bundler_test.rb / integration_test.rb / bundle composition tests in server_test.rb, which also fail on main in my environment (they need to install gems).

Manual Tests

  1. Open a file with a method like:
    def total(items, tax:)
      sum = 0
      items.each { |item| sum += item }
      { sum:, tax: }
    end
  2. Put the cursor on sum and run Find All References: you should get the three uses inside this method only.
  3. F2 on sum and rename to subtotal: the hash becomes { sum: subtotal, tax: }.
  4. F2 on tax: it is rejected because it's a keyword parameter.
  5. F12 on item inside the block jumps to the block parameter.

Resolve local variables per scope with a Prism visitor that uses the
depth Prism assigns to each local variable node. This covers reads,
writes, parameters, block locals, pattern matching, regexp named
captures and hash/keyword shorthands, and only looks at the current
document since locals can't be referenced from other files.
@Mohith26
Mohith26 requested a review from a team as a code owner November 4, 2026 13:13
@Mohith26

Mohith26 commented Nov 4, 2026

Copy link
Copy Markdown
Author

I have signed the CLA!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement find references and rename for locals Add references support for local variables

1 participant