Skip to content

Commit

Permalink
Handle keyword local variables correctly
Browse files Browse the repository at this point in the history
Local variable can be a keyword. Example: `def f(if:0, and:0); binding.irb; end`
IRB prepends local variable assignment code `a=_=nil;` to the input code but keyword local variables should be excluded.
  • Loading branch information
tompng committed Feb 13, 2025
1 parent a05231f commit d6be3b9
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 26 deletions.
24 changes: 2 additions & 22 deletions lib/irb/completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,6 @@ class BaseCompletor # :nodoc:

# Set of reserved words used by Ruby, you should not use these for
# constants or variables
ReservedWords = %w[
__ENCODING__ __LINE__ __FILE__
BEGIN END
alias and
begin break
case class
def defined? do
else elsif end ensure
false for
if in
module
next nil not
or
redo rescue retry return
self super
then true
undef unless until
when while
yield
]

HELP_COMMAND_PREPOSING = /\Ahelp\s+/

Expand Down Expand Up @@ -459,12 +439,12 @@ def retrieve_completion_data(input, bind:, doc_namespace:)
eval("#{perfect_match_var}.class.name", bind) rescue nil
else
candidates = (bind.eval_methods | bind.eval_private_methods | bind.local_variables | bind.eval_instance_variables | bind.eval_class_constants).collect{|m| m.to_s}
candidates |= ReservedWords
candidates |= RubyLex::RESERVED_WORDS.map(&:to_s)
candidates.find{ |i| i == input }
end
else
candidates = (bind.eval_methods | bind.eval_private_methods | bind.local_variables | bind.eval_instance_variables | bind.eval_class_constants).collect{|m| m.to_s}
candidates |= ReservedWords
candidates |= RubyLex::RESERVED_WORDS.map(&:to_s)
candidates.grep(/^#{Regexp.quote(input)}/).sort
end
end
Expand Down
25 changes: 25 additions & 0 deletions lib/irb/ruby-lex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ class RubyLex
on_words_beg on_qwords_beg
]

RESERVED_WORDS = %i[
__ENCODING__ __LINE__ __FILE__
BEGIN END
alias and
begin break
case class
def defined? do
else elsif end ensure
false for
if in
module
next nil not
or
redo rescue retry return
self super
then true
undef unless until
when while
yield
]

class TerminateLineInput < StandardError
def initialize
super("Terminate Line Input")
Expand All @@ -77,6 +98,10 @@ def compile_with_errors_suppressed(code, line_no: 1)
end

def generate_local_variables_assign_code(local_variables)
# Some reserved words could be a local variable
# Example: def f(if: 1); binding.irb; end
# These reserved words should be removed from assignment code
local_variables -= RESERVED_WORDS
"#{local_variables.join('=')}=nil;" unless local_variables.empty?
end

Expand Down
4 changes: 2 additions & 2 deletions test/irb/test_color.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ def test_colorize_code_with_local_variables
result_with_lvars = "a /(b +#{BLUE}#{BOLD}1#{CLEAR})/i"

assert_equal_with_term(result_without_lvars, code)
assert_equal_with_term(result_with_lvar, code, local_variables: ['a'])
assert_equal_with_term(result_with_lvars, code, local_variables: ['a', 'b'])
assert_equal_with_term(result_with_lvar, code, local_variables: [:a])
assert_equal_with_term(result_with_lvars, code, local_variables: [:a, :b])
end

def test_colorize_code_complete_true
Expand Down
14 changes: 14 additions & 0 deletions test/irb/test_irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ def test_underscore_stores_last_result
assert_include output, "=> 12"
end

def test_local_variable_handeld_correctly
write_ruby <<~'RUBY'
tap do |if:0, and:0, foo:100|
binding.irb
end
RUBY

output = run_ruby_file do
type 'foo /4#/ do'
type 'exit!'
end
assert_include output, '=> 25'
end

def test_commands_dont_override_stored_last_result
write_ruby <<~'RUBY'
binding.irb
Expand Down
12 changes: 10 additions & 2 deletions test/irb/test_ruby_lex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,16 @@ def test_local_variables_dependent_code
lines = ["a /1#/ do", "2"]
assert_indent_level(lines, 1)
assert_code_block_open(lines, true)
assert_indent_level(lines, 0, local_variables: ['a'])
assert_code_block_open(lines, false, local_variables: ['a'])
assert_indent_level(lines, 0, local_variables: [:a])
assert_code_block_open(lines, false, local_variables: [:a])
end

def test_keyword_local_variables
# Assuming `def f(if: 1, and: 2, ); binding.irb; end`
local_variables = [:if, :and]
lines = ['1 + 2']
assert_indent_level(lines, 0, local_variables: local_variables)
assert_code_block_open(lines, false, local_variables: local_variables)
end

def test_literal_ends_with_space
Expand Down

0 comments on commit d6be3b9

Please sign in to comment.