Skip to content

Add an inline variable refactor - #4252

Open
IslamElsayed wants to merge 1 commit into
Shopify:mainfrom
IslamElsayed:inline-variable
Open

IslamElsayed wants to merge 1 commit into
Shopify:mainfrom
IslamElsayed:inline-variable

Conversation

@IslamElsayed

Copy link
Copy Markdown

Motivation

Closes #2504

The Ruby LSP can extract a variable but can't inline one.

Implementation

  • CodeActions offers "Refactor: Inline Variable" (refactor.inline) when the cursor or selection is on a local variable's name, at its assignment or at a read. As with the other refactors, the resolve request does the work.

  • The resolver finds the scope that declares the variable, using Prism's depth to step out of blocks. It collects every assignment and read of the variable there, skipping block parameters that shadow it. It then replaces each read with the assigned value and removes the assignment: the whole line, or a = 1; when it shares a line.

  • Parentheses follow your suggestion, but are based on operator precedence rather than added every time. The example in the issue stays b = 5 * 10 - 5, while a = 5 - 10; b = a * 5 becomes b = (5 - 10) * 5. It also handles ** grouping to the right, negative literals, receivers ((1..3).to_a), and/or, and hashes passed without parentheses. When unsure, it adds them.

  • It refuses, with a message, when inlining could change behaviour:

    • The variable is assigned more than once (including += and multiple assignment) or is a parameter.
    • The assignment is part of an expression (foo(a = 1)), or might not run before a read (a = 1 if cond; puts a).
    • A variable the value reads is reassigned before the value is read. When a read is inside a block, any later reassignment counts.
    • The value calls methods or builds objects (strings, arrays, hashes), unless it's read exactly once, unconditionally, in the next statement. This way a call never runs more or fewer times, or later, than before.
    • The value contains a heredoc.

    The fourth rule is stricter than what I described in the issue: moving a call past other statements can change its result too.

The part most worth a careful look is needs_parentheses? and its precedence table.

Automated Tests

  • Fixtures with expectations: the example from the issue, the parentheses case, inlining from a read, and an assignment followed by ;. One more expectation checks that the action is offered.
  • test/requests/inline_variable_test.rb covers the precedence cases and each refusal.
  • The full suite, Sorbet and RuboCop pass locally on Ruby 4.0.7. The one exception is the backtrace test in server_test.rb, which expects the checkout directory to be named ruby-lsp.

Manual Tests

  1. Open a Ruby file with:
    def foo
      a = 5 - 10
      puts a
      b = a * 5
    end
  2. Put the cursor on a in a = 5 - 10, or on either read, and open the code actions menu.
  3. Choose "Refactor: Inline Variable". The body becomes puts 5 - 10 and b = (5 - 10) * 5.
  4. Change 5 - 10 to compute and try again. The action is offered but refuses with a message, because compute would run twice.

I use AI assistance when working on patches, and I check and test everything before submitting.

Offered with the cursor on a local variable's name, at its assignment or
a read. It replaces every read with the assigned value and removes the
assignment, adding parentheses only where operator precedence or the
surrounding syntax needs them.

It refuses when inlining could change behaviour: a variable assigned
more than once or by a parameter, an assignment inside an expression,
reads the assignment may not reach, a variable the value reads being
reassigned before the value is read, and values that call methods or
build objects unless they are read once, unconditionally, in the next
statement.

Closes Shopify#2504
@IslamElsayed
IslamElsayed requested a review from a team as a code owner October 28, 2026 12:35
@IslamElsayed

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.

Add inline variable refactor

1 participant