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:
parent
344e6c915f
commit
a09f764ce5
9 changed files with 109 additions and 28 deletions
13
lib/irb.rb
13
lib/irb.rb
|
@ -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"
|
||||||
|
|
|
@ -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,8 +154,13 @@ module IRB # :nodoc:
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if lvars_code
|
||||||
|
colored.sub(/\A.+\n/, '')
|
||||||
|
else
|
||||||
colored
|
colored
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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|
|
||||||
|
|
|
@ -136,17 +136,19 @@ 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|
|
||||||
lexer = Ripper::Lexer.new(inner_code, '-', line_no)
|
lexer = Ripper::Lexer.new(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
|
||||||
|
|
||||||
|
|
|
@ -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-'
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue