From 5a40f7db54dfcc7dadb75dde32c25b88c78d6a85 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 16 Aug 2023 11:13:41 +0100 Subject: [PATCH] [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 --- lib/irb.rb | 42 ++++-------------------- lib/irb/ruby-lex.rb | 25 +++++++++++--- lib/irb/statement.rb | 78 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 41 deletions(-) create mode 100644 lib/irb/statement.rb diff --git a/lib/irb.rb b/lib/irb.rb index c884d70a67..93ab6370ed 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -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| diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb index 282e6ef05f..3a0173a6be 100644 --- a/lib/irb/ruby-lex.rb +++ b/lib/irb/ruby-lex.rb @@ -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) diff --git a/lib/irb/statement.rb b/lib/irb/statement.rb new file mode 100644 index 0000000000..9493c3ffb1 --- /dev/null +++ b/lib/irb/statement.rb @@ -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