From 9fc14eb74bc447108683bc830eb169369d3994d2 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Sat, 11 Jan 2025 05:29:27 +0800 Subject: [PATCH] Gracefully handle incorrect command aliases (#1059) * Gracefully handle incorrect command aliases Even if the aliased target is a helper method or does not exist, IRB should not crash. This commit warns users in such cases and treat the input as normal expression. * Streamline command parsing and introduce warnings for incorrect command aliases --- lib/irb.rb | 16 +++---- lib/irb/context.rb | 55 +++++++++++++++++------ lib/irb/statement.rb | 21 +++++++++ test/irb/command/test_command_aliasing.rb | 50 +++++++++++++++++++++ 4 files changed, 118 insertions(+), 24 deletions(-) create mode 100644 test/irb/command/test_command_aliasing.rb diff --git a/lib/irb.rb b/lib/irb.rb index 169985773..29be6386c 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -269,29 +269,25 @@ def each_top_level_statement loop do code = readmultiline break unless code - yield build_statement(code), @line_no + yield parse_input(code), @line_no @line_no += code.count("\n") rescue RubyLex::TerminateLineInput end end - def build_statement(code) + def parse_input(code) if code.match?(/\A\n*\z/) return Statement::EmptyInput.new end code = code.dup.force_encoding(@context.io.encoding) - if (command, arg = @context.parse_command(code)) - command_class = Command.load_command(command) - Statement::Command.new(code, command_class, arg) - else - is_assignment_expression = @scanner.assignment_expression?(code, local_variables: @context.local_variables) - Statement::Expression.new(code, is_assignment_expression) - end + is_assignment_expression = @scanner.assignment_expression?(code, local_variables: @context.local_variables) + + @context.parse_input(code, is_assignment_expression) end def command?(code) - !!@context.parse_command(code) + parse_input(code).is_a?(Statement::Command) end def configure_io diff --git a/lib/irb/context.rb b/lib/irb/context.rb index c65628192..8d6545224 100644 --- a/lib/irb/context.rb +++ b/lib/irb/context.rb @@ -600,6 +600,8 @@ def evaluate(statement, line_no) # :nodoc: set_last_value(result) when Statement::Command statement.command_class.execute(self, statement.arg) + when Statement::IncorrectAlias + warn statement.message end nil @@ -633,35 +635,60 @@ def evaluate_expression(code, line_no) # :nodoc: result end - def parse_command(code) + def parse_input(code, is_assignment_expression) command_name, arg = code.strip.split(/\s+/, 2) - return unless code.lines.size == 1 && command_name - arg ||= '' - command = command_name.to_sym - # Command aliases are always command. example: $, @ - if (alias_name = command_aliases[command]) - return [alias_name, arg] + + # command can only be 1 line + if code.lines.size != 1 || + # command name is required + command_name.nil? || + # local variable have precedence over command + local_variables.include?(command_name.to_sym) || + # assignment expression is not a command + (is_assignment_expression || + (arg.start_with?(ASSIGN_OPERATORS_REGEXP) && !arg.start_with?(/==|=~/))) + return Statement::Expression.new(code, is_assignment_expression) end - # Assignment-like expression is not a command - return if arg.start_with?(ASSIGN_OPERATORS_REGEXP) && !arg.start_with?(/==|=~/) + command = command_name.to_sym - # Local variable have precedence over command - return if local_variables.include?(command) + # Check command aliases + if aliased_name = command_aliases[command] + if command_class = Command.load_command(aliased_name) + command = aliased_name + elsif HelperMethod.helper_methods[aliased_name] + message = <<~MESSAGE + Using command alias `#{command}` for helper method `#{aliased_name}` is not supported. + Please check the value of `IRB.conf[:COMMAND_ALIASES]`. + MESSAGE + return Statement::IncorrectAlias.new(message) + else + message = <<~MESSAGE + You're trying to use command alias `#{command}` for command `#{aliased_name}`, but `#{aliased_name}` does not exist. + Please check the value of `IRB.conf[:COMMAND_ALIASES]`. + MESSAGE + return Statement::IncorrectAlias.new(message) + end + else + command_class = Command.load_command(command) + end # Check visibility public_method = !!KERNEL_PUBLIC_METHOD.bind_call(main, command) rescue false private_method = !public_method && !!KERNEL_METHOD.bind_call(main, command) rescue false - if Command.execute_as_command?(command, public_method: public_method, private_method: private_method) - [command, arg] + if command_class && Command.execute_as_command?(command, public_method: public_method, private_method: private_method) + Statement::Command.new(code, command_class, arg) + else + Statement::Expression.new(code, is_assignment_expression) end end def colorize_input(input, complete:) if IRB.conf[:USE_COLORIZE] && IRB::Color.colorable? lvars = local_variables || [] - if parse_command(input) + parsed_input = parse_input(input, false) + if parsed_input.is_a?(Statement::Command) name, sep, arg = input.split(/(\s+)/, 2) arg = IRB::Color.colorize_code(arg, complete: complete, local_variables: lvars) "#{IRB::Color.colorize(name, [:BOLD])}\e[m#{sep}#{arg}" diff --git a/lib/irb/statement.rb b/lib/irb/statement.rb index 9591a4035..6a959995d 100644 --- a/lib/irb/statement.rb +++ b/lib/irb/statement.rb @@ -54,6 +54,27 @@ def is_assignment? end end + class IncorrectAlias < Statement + attr_reader :message + + def initialize(message) + @code = "" + @message = message + end + + def should_be_handled_by_debugger? + false + end + + def is_assignment? + false + end + + def suppresses_echo? + true + end + end + class Command < Statement attr_reader :command_class, :arg diff --git a/test/irb/command/test_command_aliasing.rb b/test/irb/command/test_command_aliasing.rb new file mode 100644 index 000000000..4ecc88c0a --- /dev/null +++ b/test/irb/command/test_command_aliasing.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "tempfile" +require_relative "../helper" + +module TestIRB + class CommandAliasingTest < IntegrationTestCase + def setup + super + write_rc <<~RUBY + IRB.conf[:COMMAND_ALIASES] = { + :c => :conf, # alias to helper method + :f => :foo + } + RUBY + + write_ruby <<~'RUBY' + binding.irb + RUBY + end + + def test_aliasing_to_helper_method_triggers_warning + out = run_ruby_file do + type "c" + type "exit" + end + assert_include(out, "Using command alias `c` for helper method `conf` is not supported.") + assert_not_include(out, "Maybe IRB bug!") + end + + def test_alias_to_non_existent_command_triggers_warning + message = "You're trying to use command alias `f` for command `foo`, but `foo` does not exist." + out = run_ruby_file do + type "f" + type "exit" + end + assert_include(out, message) + assert_not_include(out, "Maybe IRB bug!") + + # Local variables take precedence over command aliases + out = run_ruby_file do + type "f = 123" + type "f" + type "exit" + end + assert_not_include(out, message) + assert_not_include(out, "Maybe IRB bug!") + end + end +end