Skip to content

Commit c8b3877

Browse files
tompngst0012k0kubun
authored
Always use local variables in current context to parse code (#397)
* Use local_variables for colorize, code_block_open check, nesting_level and assignment_expression check * Check if expression is an assignment BEFORE evaluating it. evaluate might define new localvars and change result of assignment_expression? * Add local_variables dependent code test * pend local variable dependent test on truffleruby code_block_open is not working on truffleruby * Always pass context to RubyLex#lex * Rename local_variable_assign_code generator method name * Add assignment expression truncate test * Add Context#local_variables and make generate_local_variables_assign_code more simple * Update lib/irb/input-method.rb Co-authored-by: Stan Lo <stan001212@gmail.com> * Add a comment why assignment expression check should be done before evaluate Co-authored-by: Stan Lo <stan001212@gmail.com> Co-authored-by: Takashi Kokubun <takashikkbn@gmail.com>
1 parent 44bc712 commit c8b3877

File tree

9 files changed

+109
-28
lines changed

9 files changed

+109
-28
lines changed

lib/irb.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -506,13 +506,15 @@ def eval_input
506506

507507
@scanner.set_auto_indent(@context) if @context.auto_indent_mode
508508

509-
@scanner.each_top_level_statement do |line, line_no|
509+
@scanner.each_top_level_statement(@context) do |line, line_no|
510510
signal_status(:IN_EVAL) do
511511
begin
512512
line.untaint if RUBY_VERSION < '2.7'
513513
if IRB.conf[:MEASURE] && IRB.conf[:MEASURE_CALLBACKS].empty?
514514
IRB.set_measure_callback
515515
end
516+
# Assignment expression check should be done before @context.evaluate to handle code like `a /2#/ if false; a = 1`
517+
is_assignment = assignment_expression?(line)
516518
if IRB.conf[:MEASURE] && !IRB.conf[:MEASURE_CALLBACKS].empty?
517519
result = nil
518520
last_proc = proc{ result = @context.evaluate(line, line_no, exception: exc) }
@@ -529,7 +531,7 @@ def eval_input
529531
@context.evaluate(line, line_no, exception: exc)
530532
end
531533
if @context.echo?
532-
if assignment_expression?(line)
534+
if is_assignment
533535
if @context.echo_on_assignment?
534536
output_value(@context.echo_on_assignment? == :truncate)
535537
end
@@ -827,9 +829,12 @@ def assignment_expression?(line)
827829
# array of parsed expressions. The first element of each expression is the
828830
# expression's type.
829831
verbose, $VERBOSE = $VERBOSE, nil
830-
result = ASSIGNMENT_NODE_TYPES.include?(Ripper.sexp(line)&.dig(1,-1,0))
832+
code = "#{RubyLex.generate_local_variables_assign_code(@context.local_variables) || 'nil;'}\n#{line}"
833+
# Get the last node_type of the line. drop(1) is to ignore the local_variables_assign_code part.
834+
node_type = Ripper.sexp(code)&.dig(1)&.drop(1)&.dig(-1, 0)
835+
ASSIGNMENT_NODE_TYPES.include?(node_type)
836+
ensure
831837
$VERBOSE = verbose
832-
result
833838
end
834839

835840
ATTR_TTY = "\e[%sm"

lib/irb/color.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,15 @@ def colorize(text, seq, colorable: colorable?)
123123
# If `complete` is false (code is incomplete), this does not warn compile_error.
124124
# This option is needed to avoid warning a user when the compile_error is happening
125125
# because the input is not wrong but just incomplete.
126-
def colorize_code(code, complete: true, ignore_error: false, colorable: colorable?)
126+
def colorize_code(code, complete: true, ignore_error: false, colorable: colorable?, local_variables: [])
127127
return code unless colorable
128128

129129
symbol_state = SymbolState.new
130130
colored = +''
131+
lvars_code = RubyLex.generate_local_variables_assign_code(local_variables)
132+
code_with_lvars = lvars_code ? "#{lvars_code}\n#{code}" : code
131133

132-
scan(code, allow_last_error: !complete) do |token, str, expr|
134+
scan(code_with_lvars, allow_last_error: !complete) do |token, str, expr|
133135
# handle uncolorable code
134136
if token.nil?
135137
colored << Reline::Unicode.escape_for_print(str)
@@ -152,7 +154,12 @@ def colorize_code(code, complete: true, ignore_error: false, colorable: colorabl
152154
end
153155
end
154156
end
155-
colored
157+
158+
if lvars_code
159+
colored.sub(/\A.+\n/, '')
160+
else
161+
colored
162+
end
156163
end
157164

158165
private

lib/irb/context.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,5 +518,9 @@ def inspect # :nodoc:
518518
end
519519
alias __to_s__ to_s
520520
alias to_s inspect
521+
522+
def local_variables # :nodoc:
523+
workspace.binding.local_variables
524+
end
521525
end
522526
end

lib/irb/input-method.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,8 @@ def initialize
286286
if IRB.conf[:USE_COLORIZE]
287287
proc do |output, complete: |
288288
next unless IRB::Color.colorable?
289-
IRB::Color.colorize_code(output, complete: complete)
289+
lvars = IRB.CurrentContext&.local_variables || []
290+
IRB::Color.colorize_code(output, complete: complete, local_variables: lvars)
290291
end
291292
else
292293
proc do |output|

lib/irb/ruby-lex.rb

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,18 @@ def set_prompt(p = nil, &block)
136136
:on_param_error
137137
]
138138

139+
def self.generate_local_variables_assign_code(local_variables)
140+
"#{local_variables.join('=')}=nil;" unless local_variables.empty?
141+
end
142+
139143
def self.ripper_lex_without_warning(code, context: nil)
140144
verbose, $VERBOSE = $VERBOSE, nil
141-
if context
142-
lvars = context.workspace&.binding&.local_variables
143-
if lvars && !lvars.empty?
144-
code = "#{lvars.join('=')}=nil\n#{code}"
145-
line_no = 0
146-
else
147-
line_no = 1
148-
end
145+
lvars_code = generate_local_variables_assign_code(context&.local_variables || [])
146+
if lvars_code
147+
code = "#{lvars_code}\n#{code}"
148+
line_no = 0
149+
else
150+
line_no = 1
149151
end
150152

151153
compile_with_errors_suppressed(code, line_no: line_no) do |inner_code, line_no|
@@ -214,6 +216,8 @@ def check_state(code, tokens = nil, context: nil)
214216
ltype = process_literal_type(tokens)
215217
indent = process_nesting_level(tokens)
216218
continue = process_continue(tokens)
219+
lvars_code = self.class.generate_local_variables_assign_code(context&.local_variables || [])
220+
code = "#{lvars_code}\n#{code}" if lvars_code
217221
code_block_open = check_code_block(code, tokens)
218222
[ltype, indent, continue, code_block_open]
219223
end
@@ -233,13 +237,13 @@ def initialize_input
233237
@code_block_open = false
234238
end
235239

236-
def each_top_level_statement
240+
def each_top_level_statement(context)
237241
initialize_input
238242
catch(:TERM_INPUT) do
239243
loop do
240244
begin
241245
prompt
242-
unless l = lex
246+
unless l = lex(context)
243247
throw :TERM_INPUT if @line == ''
244248
else
245249
@line_no += l.count("\n")
@@ -269,18 +273,15 @@ def each_top_level_statement
269273
end
270274
end
271275

272-
def lex
276+
def lex(context)
273277
line = @input.call
274278
if @io.respond_to?(:check_termination)
275279
return line # multiline
276280
end
277281
code = @line + (line.nil? ? '' : line)
278282
code.gsub!(/\s*\z/, '').concat("\n")
279-
@tokens = self.class.ripper_lex_without_warning(code)
280-
@continue = process_continue
281-
@code_block_open = check_code_block(code)
282-
@indent = process_nesting_level
283-
@ltype = process_literal_type
283+
@tokens = self.class.ripper_lex_without_warning(code, context: context)
284+
@ltype, @indent, @continue, @code_block_open = check_state(code, @tokens, context: context)
284285
line
285286
end
286287

test/irb/test_color.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,17 @@ def test_colorize_code
156156
end
157157
end
158158

159+
def test_colorize_code_with_local_variables
160+
code = "a /(b +1)/i"
161+
result_without_lvars = "a #{RED}#{BOLD}/#{CLEAR}#{RED}(b +1)#{CLEAR}#{RED}#{BOLD}/i#{CLEAR}"
162+
result_with_lvar = "a /(b #{BLUE}#{BOLD}+1#{CLEAR})/i"
163+
result_with_lvars = "a /(b +#{BLUE}#{BOLD}1#{CLEAR})/i"
164+
165+
assert_equal_with_term(result_without_lvars, code)
166+
assert_equal_with_term(result_with_lvar, code, local_variables: ['a'])
167+
assert_equal_with_term(result_with_lvars, code, local_variables: ['a', 'b'])
168+
end
169+
159170
def test_colorize_code_complete_true
160171
unless complete_option_supported?
161172
pend '`complete: true` is the same as `complete: false` in Ruby 2.6-'

test/irb/test_context.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,16 @@ def test_assignment_expression
225225
end
226226
end
227227

228+
def test_assignment_expression_with_local_variable
229+
input = TestInputMethod.new
230+
irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input)
231+
code = "a /1;x=1#/"
232+
refute(irb.assignment_expression?(code), "#{code}: should not be an assignment expression")
233+
irb.context.workspace.binding.eval('a = 1')
234+
assert(irb.assignment_expression?(code), "#{code}: should be an assignment expression")
235+
refute(irb.assignment_expression?(""), "empty code should not be an assignment expression")
236+
end
237+
228238
def test_echo_on_assignment
229239
input = TestInputMethod.new([
230240
"a = 1\n",

test/irb/test_ruby_lex.rb

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,27 @@ def assert_indenting(lines, correct_space_count, add_new_line)
3434
ruby_lex.set_auto_indent(context)
3535
end
3636

37-
def assert_nesting_level(lines, expected)
37+
def assert_nesting_level(lines, expected, local_variables: [])
38+
ruby_lex = ruby_lex_for_lines(lines, local_variables: local_variables)
39+
error_message = "Calculated the wrong number of nesting level for:\n #{lines.join("\n")}"
40+
assert_equal(expected, ruby_lex.instance_variable_get(:@indent), error_message)
41+
end
42+
43+
def assert_code_block_open(lines, expected, local_variables: [])
44+
ruby_lex = ruby_lex_for_lines(lines, local_variables: local_variables)
45+
error_message = "Wrong result of code_block_open for:\n #{lines.join("\n")}"
46+
assert_equal(expected, ruby_lex.instance_variable_get(:@code_block_open), error_message)
47+
end
48+
49+
def ruby_lex_for_lines(lines, local_variables: [])
3850
ruby_lex = RubyLex.new()
3951
io = proc{ lines.join("\n") }
4052
ruby_lex.set_input(io, io)
41-
ruby_lex.lex
42-
error_message = "Calculated the wrong number of nesting level for:\n #{lines.join("\n")}"
43-
assert_equal(expected, ruby_lex.instance_variable_get(:@indent), error_message)
53+
unless local_variables.empty?
54+
context = OpenStruct.new(local_variables: local_variables)
55+
end
56+
ruby_lex.lex(context)
57+
ruby_lex
4458
end
4559

4660
def test_auto_indent
@@ -514,6 +528,15 @@ def test_do_corresponding_to_loop
514528
end
515529
end
516530

531+
def test_local_variables_dependent_code
532+
pend if RUBY_ENGINE == 'truffleruby'
533+
lines = ["a /1#/ do", "2"]
534+
assert_nesting_level(lines, 1)
535+
assert_code_block_open(lines, true)
536+
assert_nesting_level(lines, 0, local_variables: ['a'])
537+
assert_code_block_open(lines, false, local_variables: ['a'])
538+
end
539+
517540
def test_heredoc_with_indent
518541
input_with_correct_indents = [
519542
Row.new(%q(<<~Q), 0, 0, 0),

test/irb/yamatanooroti/test_rendering.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,25 @@ def test_autocomplete_with_showdoc_in_gaps_on_narrow_screen_left
216216
EOC
217217
end
218218

219+
def test_assignment_expression_truncate
220+
write_irbrc <<~'LINES'
221+
puts 'start IRB'
222+
LINES
223+
start_terminal(40, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB')
224+
# Assignment expression code that turns into non-assignment expression after evaluation
225+
code = "a /'/i if false; a=1; x=1000.times.to_a#'.size"
226+
write(code + "\n")
227+
close
228+
assert_screen(<<~EOC)
229+
start IRB
230+
irb(main):001:0> #{code}
231+
=>
232+
[0,
233+
...
234+
irb(main):002:0>
235+
EOC
236+
end
237+
219238
private def write_irbrc(content)
220239
File.open(@irbrc_file, 'w') do |f|
221240
f.write content

0 commit comments

Comments
 (0)