Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions lib/irb/completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ def retrieve_completion_data(input, bind:, doc_namespace:)
rescue EncodingError
# ignore
end
candidates.grep(/^#{Regexp.quote(sym)}/)
safe_grep(candidates, /^#{Regexp.quote(sym)}/)
end
when /^::([A-Z][^:\.\(\)]*)$/
# Absolute Constant or class methods
Expand All @@ -291,7 +291,7 @@ def retrieve_completion_data(input, bind:, doc_namespace:)
if doc_namespace
candidates.find { |i| i == receiver }
else
candidates.grep(/^#{Regexp.quote(receiver)}/).collect{|e| "::" + e}
safe_grep(candidates, /^#{Regexp.quote(receiver)}/).collect{|e| "::" + e}
end

when /^([A-Z].*)::([^:.]*)$/
Expand Down Expand Up @@ -378,7 +378,7 @@ def retrieve_completion_data(input, bind:, doc_namespace:)
if doc_namespace
all_gvars.find{ |i| i == gvar }
else
all_gvars.grep(Regexp.new(Regexp.quote(gvar)))
safe_grep(all_gvars, Regexp.new(Regexp.quote(gvar)))
end

when /^([^.:"].*)(\.|::)([^.]*)$/
Expand Down Expand Up @@ -453,16 +453,30 @@ def retrieve_completion_data(input, bind:, doc_namespace:)
else
candidates = (bind.eval_methods | bind.eval_private_methods | bind.local_variables | bind.eval_instance_variables | bind.eval_class_constants).collect{|m| m.to_s}
candidates |= RubyLex::RESERVED_WORDS.map(&:to_s)
candidates.grep(/^#{Regexp.quote(input)}/).sort
safe_grep(candidates, /^#{Regexp.quote(input)}/).sort
end
end
end

# Set of available operators in Ruby
Operators = %w[% & * ** + - / < << <= <=> == === =~ > >= >> [] []= ^ ! != !~]

def safe_grep(candidates, pattern)
target_encoding = Encoding.default_external
candidates.filter_map do |candidate|
next unless candidate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is not needed.
Array#grep accepts nil, but all candidates passed to this method is always an array of string.

candidates = something.collect{|m| m.to_s}
safe_grep(candidates, pattern)

Copy link
Author

@alexanderadam alexanderadam Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was surprised as well but without this the tests fail on TruffleRuby (compare the builds on the two commits in this PR).

It's working on every other platform though. 🤔
Or was I missing a bit here or misunderstanding something?

/CC @eregon

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, my comment "always an array of string" was wrong.

# Line 278
candidates = Symbol.all_symbols.collect do |s|
  s.inspect
rescue EncodingError
  # ignore (this part creates nil)
end
safe_grep(candidates, /^#{Regexp.quote(sym)}/)

Changing this collect to filter_map to reject nil seems better.


converted = candidate.encoding == target_encoding ? candidate : candidate.encode(target_encoding)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encoding conversion is more than safe-grep. I think this method should only do safe grep (match? and rescue) for simplicity.

Encoding conversion is already done in line 205.

def completion_candidates(preposing, target, postposing, bind:)
  ...
  completion_data = retrieve_completion_data(target, bind: bind, doc_namespace: false).compact.filter_map do |i|
    i.encode(Encoding.default_external)
  rescue Encoding::UndefinedConversionError
    # If the string cannot be converted, we just ignore it
    nil
  end
  ...
end


converted if pattern.match?(converted)
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError, Encoding::CompatibilityError, EncodingError
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError and Encoding::CompatibilityError are all subclass of EncodingError.
Simply rescue EncodingError is enough, or rescue Encoding::CompatibilityError if match? is the only method called.

# Skip candidates that cannot be converted to the target encoding
nil
end
end

def select_message(receiver, message, candidates, sep = ".")
candidates.grep(/^#{Regexp.quote(message)}/).collect do |e|
safe_grep(candidates, /^#{Regexp.quote(message)}/).collect do |e|
case e
when /^[a-zA-Z_]/
receiver + sep + e
Expand Down
13 changes: 13 additions & 0 deletions test/irb/test_completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -343,5 +343,18 @@ def test_regexp_completor_handles_encoding_errors_gracefully
Encoding.default_external = original_encoding
end
end

def test_utf16_method_name_does_not_crash
# Reproduces issue #52: https://github.com/ruby/irb/issues/52
method_name = "a".encode(Encoding::UTF_16)
test_obj = Object.new
test_obj.define_singleton_method(method_name) {}
test_bind = test_obj.instance_eval { binding }

completor = IRB::RegexpCompletor.new
assert_nothing_raised do
completor.completion_candidates('', 'a', '', bind: test_bind)
end
end
end
end