[ruby/irb] Encapsulate input details in Statement objects

(https://github.com/ruby/irb/pull/682)

* Introduce Statement class

* Split Statement class for better clarity

https://github.com/ruby/irb/commit/65e8e68690
This commit is contained in:
Stan Lo 2023-08-16 11:13:41 +01:00 коммит произвёл git
Родитель 0982c5fa00
Коммит 5a40f7db54
3 изменённых файлов: 104 добавлений и 41 удалений

Просмотреть файл

@ -570,26 +570,19 @@ module IRB
configure_io
@scanner.each_top_level_statement do |line, line_no, is_assignment|
@scanner.each_top_level_statement do |statement, line_no|
signal_status(:IN_EVAL) do
begin
# If the integration with debugger is activated, we need to handle certain input differently
if @context.with_debugger
command_class = load_command_class(line)
# First, let's pass debugging command's input to debugger
# Secondly, we need to let debugger evaluate non-command input
# Otherwise, the expression will be evaluated in the debugger's main session thread
# This is the only way to run the user's program in the expected thread
if !command_class || ExtendCommand::DebugCommand > command_class
return line
end
if @context.with_debugger && statement.should_be_handled_by_debugger?
return statement.code
end
evaluate_line(line, line_no)
@context.evaluate(statement.evaluable_code, line_no)
# Don't echo if the line ends with a semicolon
if @context.echo? && !line.match?(/;\s*\z/)
if is_assignment
if @context.echo? && !statement.suppresses_echo?
if statement.is_assignment?
if @context.echo_on_assignment?
output_value(@context.echo_on_assignment? == :truncate)
end
@ -659,29 +652,6 @@ module IRB
end
end
def evaluate_line(line, line_no)
# Transform a non-identifier alias (@, $) or keywords (next, break)
command, args = line.split(/\s/, 2)
if original = @context.command_aliases[command.to_sym]
line = line.gsub(/\A#{Regexp.escape(command)}/, original.to_s)
command = original
end
# Hook command-specific transformation
command_class = ExtendCommandBundle.load_command(command)
if command_class&.respond_to?(:transform_args)
line = "#{command} #{command_class.transform_args(args)}"
end
@context.evaluate(line, line_no)
end
def load_command_class(line)
command, _ = line.split(/\s/, 2)
command_name = @context.command_aliases[command.to_sym]
ExtendCommandBundle.load_command(command_name || command)
end
def convert_invalid_byte_sequence(str, enc)
str.force_encoding(enc)
str.scrub { |c|

Просмотреть файл

@ -7,6 +7,7 @@
require "ripper"
require "jruby" if RUBY_ENGINE == "jruby"
require_relative "nesting_parser"
require_relative "statement"
# :stopdoc:
class RubyLex
@ -221,16 +222,30 @@ class RubyLex
break unless code
if code != "\n"
code.force_encoding(@context.io.encoding)
yield code, @line_no, assignment_expression?(code)
yield build_statement(code), @line_no
end
increase_line_no(code.count("\n"))
rescue TerminateLineInput
end
end
def assignment_expression?(line)
# Try to parse the line and check if the last of possibly multiple
def build_statement(code)
code.force_encoding(@context.io.encoding)
command_or_alias, arg = code.split(/\s/, 2)
# Transform a non-identifier alias (@, $) or keywords (next, break)
command_name = @context.command_aliases[command_or_alias.to_sym]
command = command_name || command_or_alias
command_class = IRB::ExtendCommandBundle.load_command(command)
if command_class
IRB::Statement::Command.new(code, command, arg, command_class)
else
IRB::Statement::Expression.new(code, assignment_expression?(code))
end
end
def assignment_expression?(code)
# Try to parse the code and check if the last of possibly multiple
# expressions is an assignment type.
# If the expression is invalid, Ripper.sexp should return nil which will
@ -239,7 +254,7 @@ class RubyLex
# array of parsed expressions. The first element of each expression is the
# expression's type.
verbose, $VERBOSE = $VERBOSE, nil
code = "#{RubyLex.generate_local_variables_assign_code(@context.local_variables) || 'nil;'}\n#{line}"
code = "#{RubyLex.generate_local_variables_assign_code(@context.local_variables) || 'nil;'}\n#{code}"
# 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)

78
lib/irb/statement.rb Normal file
Просмотреть файл

@ -0,0 +1,78 @@
# frozen_string_literal: true
module IRB
class Statement
attr_reader :code
def is_assignment?
raise NotImplementedError
end
def suppresses_echo?
raise NotImplementedError
end
def should_be_handled_by_debugger?
raise NotImplementedError
end
def evaluable_code
raise NotImplementedError
end
class Expression < Statement
def initialize(code, is_assignment)
@code = code
@is_assignment = is_assignment
end
def suppresses_echo?
@code.match?(/;\s*\z/)
end
def should_be_handled_by_debugger?
true
end
def is_assignment?
@is_assignment
end
def evaluable_code
@code
end
end
class Command < Statement
def initialize(code, command, arg, command_class)
@code = code
@command = command
@arg = arg
@command_class = command_class
end
def is_assignment?
false
end
def suppresses_echo?
false
end
def should_be_handled_by_debugger?
IRB::ExtendCommand::DebugCommand > @command_class
end
def evaluable_code
# Hook command-specific transformation to return valid Ruby code
if @command_class.respond_to?(:transform_args)
arg = @command_class.transform_args(@arg)
else
arg = @arg
end
[@command, arg].compact.join(' ')
end
end
end
end