[ruby/irb] Gracefully handle incorrect command aliases

(https://github.com/ruby/irb/pull/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

https://github.com/ruby/irb/commit/9fc14eb74b
This commit is contained in:
Stan Lo 2025-01-11 05:29:27 +08:00 committed by git
parent 4a2702dafb
commit 039446f601
4 changed files with 118 additions and 24 deletions

View File

@ -269,29 +269,25 @@ module IRB
loop do loop do
code = readmultiline code = readmultiline
break unless code break unless code
yield build_statement(code), @line_no yield parse_input(code), @line_no
@line_no += code.count("\n") @line_no += code.count("\n")
rescue RubyLex::TerminateLineInput rescue RubyLex::TerminateLineInput
end end
end end
def build_statement(code) def parse_input(code)
if code.match?(/\A\n*\z/) if code.match?(/\A\n*\z/)
return Statement::EmptyInput.new return Statement::EmptyInput.new
end end
code = code.dup.force_encoding(@context.io.encoding) code = code.dup.force_encoding(@context.io.encoding)
if (command, arg = @context.parse_command(code)) is_assignment_expression = @scanner.assignment_expression?(code, local_variables: @context.local_variables)
command_class = Command.load_command(command)
Statement::Command.new(code, command_class, arg) @context.parse_input(code, is_assignment_expression)
else
is_assignment_expression = @scanner.assignment_expression?(code, local_variables: @context.local_variables)
Statement::Expression.new(code, is_assignment_expression)
end
end end
def command?(code) def command?(code)
!!@context.parse_command(code) parse_input(code).is_a?(Statement::Command)
end end
def configure_io def configure_io

View File

@ -600,6 +600,8 @@ module IRB
set_last_value(result) set_last_value(result)
when Statement::Command when Statement::Command
statement.command_class.execute(self, statement.arg) statement.command_class.execute(self, statement.arg)
when Statement::IncorrectAlias
warn statement.message
end end
nil nil
@ -633,35 +635,60 @@ module IRB
result result
end end
def parse_command(code) def parse_input(code, is_assignment_expression)
command_name, arg = code.strip.split(/\s+/, 2) command_name, arg = code.strip.split(/\s+/, 2)
return unless code.lines.size == 1 && command_name
arg ||= '' arg ||= ''
command = command_name.to_sym
# Command aliases are always command. example: $, @ # command can only be 1 line
if (alias_name = command_aliases[command]) if code.lines.size != 1 ||
return [alias_name, arg] # 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 end
# Assignment-like expression is not a command command = command_name.to_sym
return if arg.start_with?(ASSIGN_OPERATORS_REGEXP) && !arg.start_with?(/==|=~/)
# Local variable have precedence over command # Check command aliases
return if local_variables.include?(command) 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 # Check visibility
public_method = !!KERNEL_PUBLIC_METHOD.bind_call(main, command) rescue false public_method = !!KERNEL_PUBLIC_METHOD.bind_call(main, command) rescue false
private_method = !public_method && !!KERNEL_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) if command_class && Command.execute_as_command?(command, public_method: public_method, private_method: private_method)
[command, arg] Statement::Command.new(code, command_class, arg)
else
Statement::Expression.new(code, is_assignment_expression)
end end
end end
def colorize_input(input, complete:) def colorize_input(input, complete:)
if IRB.conf[:USE_COLORIZE] && IRB::Color.colorable? if IRB.conf[:USE_COLORIZE] && IRB::Color.colorable?
lvars = local_variables || [] 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) name, sep, arg = input.split(/(\s+)/, 2)
arg = IRB::Color.colorize_code(arg, complete: complete, local_variables: lvars) arg = IRB::Color.colorize_code(arg, complete: complete, local_variables: lvars)
"#{IRB::Color.colorize(name, [:BOLD])}\e[m#{sep}#{arg}" "#{IRB::Color.colorize(name, [:BOLD])}\e[m#{sep}#{arg}"

View File

@ -54,6 +54,27 @@ module IRB
end end
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 class Command < Statement
attr_reader :command_class, :arg attr_reader :command_class, :arg

View File

@ -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