1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00

[ruby/irb] Always use local variables in current context to parse code (https://github.com/ruby/irb/pull/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

https://github.com/ruby/irb/commit/c8b3877281

Co-authored-by: Stan Lo <stan001212@gmail.com>
Co-authored-by: Takashi Kokubun <takashikkbn@gmail.com>
This commit is contained in:
tomoya ishida 2022-10-18 14:44:04 +09:00 committed by git
parent 344e6c915f
commit a09f764ce5
9 changed files with 109 additions and 28 deletions

View file

@ -506,13 +506,15 @@ module IRB
@scanner.set_auto_indent(@context) if @context.auto_indent_mode @scanner.set_auto_indent(@context) if @context.auto_indent_mode
@scanner.each_top_level_statement do |line, line_no| @scanner.each_top_level_statement(@context) do |line, line_no|
signal_status(:IN_EVAL) do signal_status(:IN_EVAL) do
begin begin
line.untaint if RUBY_VERSION < '2.7' line.untaint if RUBY_VERSION < '2.7'
if IRB.conf[:MEASURE] && IRB.conf[:MEASURE_CALLBACKS].empty? if IRB.conf[:MEASURE] && IRB.conf[:MEASURE_CALLBACKS].empty?
IRB.set_measure_callback IRB.set_measure_callback
end end
# Assignment expression check should be done before @context.evaluate to handle code like `a /2#/ if false; a = 1`
is_assignment = assignment_expression?(line)
if IRB.conf[:MEASURE] && !IRB.conf[:MEASURE_CALLBACKS].empty? if IRB.conf[:MEASURE] && !IRB.conf[:MEASURE_CALLBACKS].empty?
result = nil result = nil
last_proc = proc{ result = @context.evaluate(line, line_no, exception: exc) } last_proc = proc{ result = @context.evaluate(line, line_no, exception: exc) }
@ -529,7 +531,7 @@ module IRB
@context.evaluate(line, line_no, exception: exc) @context.evaluate(line, line_no, exception: exc)
end end
if @context.echo? if @context.echo?
if assignment_expression?(line) if is_assignment
if @context.echo_on_assignment? if @context.echo_on_assignment?
output_value(@context.echo_on_assignment? == :truncate) output_value(@context.echo_on_assignment? == :truncate)
end end
@ -827,9 +829,12 @@ module IRB
# array of parsed expressions. The first element of each expression is the # array of parsed expressions. The first element of each expression is the
# expression's type. # expression's type.
verbose, $VERBOSE = $VERBOSE, nil verbose, $VERBOSE = $VERBOSE, nil
result = ASSIGNMENT_NODE_TYPES.include?(Ripper.sexp(line)&.dig(1,-1,0)) code = "#{RubyLex.generate_local_variables_assign_code(@context.local_variables) || 'nil;'}\n#{line}"
# Get the last node_type of the line. drop(1) is to ignore the local_variables_assign_code part.
node_type = Ripper.sexp(code)&.dig(1)&.drop(1)&.dig(-1, 0)
ASSIGNMENT_NODE_TYPES.include?(node_type)
ensure
$VERBOSE = verbose $VERBOSE = verbose
result
end end
ATTR_TTY = "\e[%sm" ATTR_TTY = "\e[%sm"

View file

@ -123,13 +123,15 @@ module IRB # :nodoc:
# If `complete` is false (code is incomplete), this does not warn compile_error. # If `complete` is false (code is incomplete), this does not warn compile_error.
# This option is needed to avoid warning a user when the compile_error is happening # This option is needed to avoid warning a user when the compile_error is happening
# because the input is not wrong but just incomplete. # because the input is not wrong but just incomplete.
def colorize_code(code, complete: true, ignore_error: false, colorable: colorable?) def colorize_code(code, complete: true, ignore_error: false, colorable: colorable?, local_variables: [])
return code unless colorable return code unless colorable
symbol_state = SymbolState.new symbol_state = SymbolState.new
colored = +'' colored = +''
lvars_code = RubyLex.generate_local_variables_assign_code(local_variables)
code_with_lvars = lvars_code ? "#{lvars_code}\n#{code}" : code
scan(code, allow_last_error: !complete) do |token, str, expr| scan(code_with_lvars, allow_last_error: !complete) do |token, str, expr|
# handle uncolorable code # handle uncolorable code
if token.nil? if token.nil?
colored << Reline::Unicode.escape_for_print(str) colored << Reline::Unicode.escape_for_print(str)
@ -152,7 +154,12 @@ module IRB # :nodoc:
end end
end end
end end
colored
if lvars_code
colored.sub(/\A.+\n/, '')
else
colored
end
end end
private private

View file

@ -518,5 +518,9 @@ module IRB
end end
alias __to_s__ to_s alias __to_s__ to_s
alias to_s inspect alias to_s inspect
def local_variables # :nodoc:
workspace.binding.local_variables
end
end end
end end

View file

@ -286,7 +286,8 @@ module IRB
if IRB.conf[:USE_COLORIZE] if IRB.conf[:USE_COLORIZE]
proc do |output, complete: | proc do |output, complete: |
next unless IRB::Color.colorable? next unless IRB::Color.colorable?
IRB::Color.colorize_code(output, complete: complete) lvars = IRB.CurrentContext&.local_variables || []
IRB::Color.colorize_code(output, complete: complete, local_variables: lvars)
end end
else else
proc do |output| proc do |output|

View file

@ -136,16 +136,18 @@ class RubyLex
:on_param_error :on_param_error
] ]
def self.generate_local_variables_assign_code(local_variables)
"#{local_variables.join('=')}=nil;" unless local_variables.empty?
end
def self.ripper_lex_without_warning(code, context: nil) def self.ripper_lex_without_warning(code, context: nil)
verbose, $VERBOSE = $VERBOSE, nil verbose, $VERBOSE = $VERBOSE, nil
if context lvars_code = generate_local_variables_assign_code(context&.local_variables || [])
lvars = context.workspace&.binding&.local_variables if lvars_code
if lvars && !lvars.empty? code = "#{lvars_code}\n#{code}"
code = "#{lvars.join('=')}=nil\n#{code}" line_no = 0
line_no = 0 else
else line_no = 1
line_no = 1
end
end end
compile_with_errors_suppressed(code, line_no: line_no) do |inner_code, line_no| compile_with_errors_suppressed(code, line_no: line_no) do |inner_code, line_no|
@ -214,6 +216,8 @@ class RubyLex
ltype = process_literal_type(tokens) ltype = process_literal_type(tokens)
indent = process_nesting_level(tokens) indent = process_nesting_level(tokens)
continue = process_continue(tokens) continue = process_continue(tokens)
lvars_code = self.class.generate_local_variables_assign_code(context&.local_variables || [])
code = "#{lvars_code}\n#{code}" if lvars_code
code_block_open = check_code_block(code, tokens) code_block_open = check_code_block(code, tokens)
[ltype, indent, continue, code_block_open] [ltype, indent, continue, code_block_open]
end end
@ -233,13 +237,13 @@ class RubyLex
@code_block_open = false @code_block_open = false
end end
def each_top_level_statement def each_top_level_statement(context)
initialize_input initialize_input
catch(:TERM_INPUT) do catch(:TERM_INPUT) do
loop do loop do
begin begin
prompt prompt
unless l = lex unless l = lex(context)
throw :TERM_INPUT if @line == '' throw :TERM_INPUT if @line == ''
else else
@line_no += l.count("\n") @line_no += l.count("\n")
@ -269,18 +273,15 @@ class RubyLex
end end
end end
def lex def lex(context)
line = @input.call line = @input.call
if @io.respond_to?(:check_termination) if @io.respond_to?(:check_termination)
return line # multiline return line # multiline
end end
code = @line + (line.nil? ? '' : line) code = @line + (line.nil? ? '' : line)
code.gsub!(/\s*\z/, '').concat("\n") code.gsub!(/\s*\z/, '').concat("\n")
@tokens = self.class.ripper_lex_without_warning(code) @tokens = self.class.ripper_lex_without_warning(code, context: context)
@continue = process_continue @ltype, @indent, @continue, @code_block_open = check_state(code, @tokens, context: context)
@code_block_open = check_code_block(code)
@indent = process_nesting_level
@ltype = process_literal_type
line line
end end

View file

@ -156,6 +156,17 @@ module TestIRB
end end
end end
def test_colorize_code_with_local_variables
code = "a /(b +1)/i"
result_without_lvars = "a #{RED}#{BOLD}/#{CLEAR}#{RED}(b +1)#{CLEAR}#{RED}#{BOLD}/i#{CLEAR}"
result_with_lvar = "a /(b #{BLUE}#{BOLD}+1#{CLEAR})/i"
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'])
end
def test_colorize_code_complete_true def test_colorize_code_complete_true
unless complete_option_supported? unless complete_option_supported?
pend '`complete: true` is the same as `complete: false` in Ruby 2.6-' pend '`complete: true` is the same as `complete: false` in Ruby 2.6-'

View file

@ -225,6 +225,16 @@ module TestIRB
end end
end end
def test_assignment_expression_with_local_variable
input = TestInputMethod.new
irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input)
code = "a /1;x=1#/"
refute(irb.assignment_expression?(code), "#{code}: should not be an assignment expression")
irb.context.workspace.binding.eval('a = 1')
assert(irb.assignment_expression?(code), "#{code}: should be an assignment expression")
refute(irb.assignment_expression?(""), "empty code should not be an assignment expression")
end
def test_echo_on_assignment def test_echo_on_assignment
input = TestInputMethod.new([ input = TestInputMethod.new([
"a = 1\n", "a = 1\n",

View file

@ -34,13 +34,27 @@ module TestIRB
ruby_lex.set_auto_indent(context) ruby_lex.set_auto_indent(context)
end end
def assert_nesting_level(lines, expected) def assert_nesting_level(lines, expected, local_variables: [])
ruby_lex = ruby_lex_for_lines(lines, local_variables: local_variables)
error_message = "Calculated the wrong number of nesting level for:\n #{lines.join("\n")}"
assert_equal(expected, ruby_lex.instance_variable_get(:@indent), error_message)
end
def assert_code_block_open(lines, expected, local_variables: [])
ruby_lex = ruby_lex_for_lines(lines, local_variables: local_variables)
error_message = "Wrong result of code_block_open for:\n #{lines.join("\n")}"
assert_equal(expected, ruby_lex.instance_variable_get(:@code_block_open), error_message)
end
def ruby_lex_for_lines(lines, local_variables: [])
ruby_lex = RubyLex.new() ruby_lex = RubyLex.new()
io = proc{ lines.join("\n") } io = proc{ lines.join("\n") }
ruby_lex.set_input(io, io) ruby_lex.set_input(io, io)
ruby_lex.lex unless local_variables.empty?
error_message = "Calculated the wrong number of nesting level for:\n #{lines.join("\n")}" context = OpenStruct.new(local_variables: local_variables)
assert_equal(expected, ruby_lex.instance_variable_get(:@indent), error_message) end
ruby_lex.lex(context)
ruby_lex
end end
def test_auto_indent def test_auto_indent
@ -514,6 +528,15 @@ module TestIRB
end end
end end
def test_local_variables_dependent_code
pend if RUBY_ENGINE == 'truffleruby'
lines = ["a /1#/ do", "2"]
assert_nesting_level(lines, 1)
assert_code_block_open(lines, true)
assert_nesting_level(lines, 0, local_variables: ['a'])
assert_code_block_open(lines, false, local_variables: ['a'])
end
def test_heredoc_with_indent def test_heredoc_with_indent
input_with_correct_indents = [ input_with_correct_indents = [
Row.new(%q(<<~Q), 0, 0, 0), Row.new(%q(<<~Q), 0, 0, 0),

View file

@ -216,6 +216,25 @@ begin
EOC EOC
end end
def test_assignment_expression_truncate
write_irbrc <<~'LINES'
puts 'start IRB'
LINES
start_terminal(40, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB')
# Assignment expression code that turns into non-assignment expression after evaluation
code = "a /'/i if false; a=1; x=1000.times.to_a#'.size"
write(code + "\n")
close
assert_screen(<<~EOC)
start IRB
irb(main):001:0> #{code}
=>
[0,
...
irb(main):002:0>
EOC
end
private def write_irbrc(content) private def write_irbrc(content)
File.open(@irbrc_file, 'w') do |f| File.open(@irbrc_file, 'w') do |f|
f.write content f.write content