[ruby/irb] Type based completion using Prism and RBS

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

* Add completor using prism and rbs

* Add TypeCompletion test

* Switchable completors: RegexpCompletor and TypeCompletion::Completor

* Add completion info to irb_info

* Complete reserved words

* Fix [*] (*) {**} and prism's change of KeywordParameterNode

* Fix require, frozen_string_literal

* Drop prism<=0.16.0 support

* Add Completor.last_completion_error for debug report

* Retrieve `self` and `Module.nesting` in more safe way

* Support BasicObject

* Handle lvar and ivar get exception correctly

* Skip ivar reference test of non-self object in ruby < 3.2

* BaseScope to RootScope, move method objects constant under Methods

* Remove unused Splat struct

* Drop deeply nested array/hash type calculation from actual object. Now, calculation depth is 1

* Refactor loading rbs in test, change preload_in_thread not to cache Thread object

* Use new option added in prism 0.17.1 to parse code with localvars

* Add Prism version check and warn when :type completor cannot be enabled

* build_type_completor should skip truffleruby (because endless method definition is not supported)

https://github.com/ruby/irb/commit/1048c7ed7a
This commit is contained in:
tomoya ishida 2023-11-08 11:46:24 +09:00 коммит произвёл git
Родитель 7ed37388fb
Коммит e344010465
18 изменённых файлов: 3446 добавлений и 32 удалений

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

@ -140,6 +140,10 @@ require_relative "irb/debug"
#
# IRB.conf[:USE_AUTOCOMPLETE] = false
#
# To enable enhanced completion using type information, add the following to your +.irbrc+:
#
# IRB.conf[:COMPLETOR] = :type
#
# === History
#
# By default, irb will store the last 1000 commands you used in

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

@ -14,6 +14,7 @@ module IRB
str = "Ruby version: #{RUBY_VERSION}\n"
str += "IRB version: #{IRB.version}\n"
str += "InputMethod: #{IRB.CurrentContext.io.inspect}\n"
str += "Completion: #{IRB.CurrentContext.io.respond_to?(:completion_info) ? IRB.CurrentContext.io.completion_info : 'off'}\n"
str += ".irbrc path: #{IRB.rc_file}\n" if File.exist?(IRB.rc_file)
str += "RUBY_PLATFORM: #{RUBY_PLATFORM}\n"
str += "LANG env: #{ENV["LANG"]}\n" if ENV["LANG"] && !ENV["LANG"].empty?

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

@ -9,6 +9,30 @@ require_relative 'ruby-lex'
module IRB
class BaseCompletor # :nodoc:
# Set of reserved words used by Ruby, you should not use these for
# constants or variables
ReservedWords = %w[
__ENCODING__ __LINE__ __FILE__
BEGIN END
alias and
begin break
case class
def defined? do
else elsif end ensure
false for
if in
module
next nil not
or
redo rescue retry return
self super
then true
undef unless until
when while
yield
]
def completion_candidates(preposing, target, postposing, bind:)
raise NotImplementedError
end
@ -94,28 +118,9 @@ module IRB
end
}
# Set of reserved words used by Ruby, you should not use these for
# constants or variables
ReservedWords = %w[
__ENCODING__ __LINE__ __FILE__
BEGIN END
alias and
begin break
case class
def defined? do
else elsif end ensure
false for
if in
module
next nil not
or
redo rescue retry return
self super
then true
undef unless until
when while
yield
]
def inspect
'RegexpCompletor'
end
def complete_require_path(target, preposing, postposing)
if target =~ /\A(['"])([^'"]+)\Z/

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

@ -86,14 +86,14 @@ module IRB
when nil
if STDIN.tty? && IRB.conf[:PROMPT_MODE] != :INF_RUBY && !use_singleline?
# Both of multiline mode and singleline mode aren't specified.
@io = RelineInputMethod.new
@io = RelineInputMethod.new(build_completor)
else
@io = nil
end
when false
@io = nil
when true
@io = RelineInputMethod.new
@io = RelineInputMethod.new(build_completor)
end
unless @io
case use_singleline?
@ -149,6 +149,43 @@ module IRB
@command_aliases = IRB.conf[:COMMAND_ALIASES]
end
private def build_completor
completor_type = IRB.conf[:COMPLETOR]
case completor_type
when :regexp
return RegexpCompletor.new
when :type
completor = build_type_completor
return completor if completor
else
warn "Invalid value for IRB.conf[:COMPLETOR]: #{completor_type}"
end
# Fallback to RegexpCompletor
RegexpCompletor.new
end
TYPE_COMPLETION_REQUIRED_PRISM_VERSION = '0.17.1'
private def build_type_completor
unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0.0') && RUBY_ENGINE != 'truffleruby'
warn 'TypeCompletion requires RUBY_VERSION >= 3.0.0'
return
end
begin
require 'prism'
rescue LoadError => e
warn "TypeCompletion requires Prism: #{e.message}"
return
end
unless Gem::Version.new(Prism::VERSION) >= Gem::Version.new(TYPE_COMPLETION_REQUIRED_PRISM_VERSION)
warn "TypeCompletion requires Prism::VERSION >= #{TYPE_COMPLETION_REQUIRED_PRISM_VERSION}"
return
end
require 'irb/type_completion/completor'
TypeCompletion::Types.preload_in_thread
TypeCompletion::Completor.new
end
def save_history=(val)
IRB.conf[:SAVE_HISTORY] = val
end

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

@ -76,6 +76,7 @@ module IRB # :nodoc:
@CONF[:USE_SINGLELINE] = false unless defined?(ReadlineInputMethod)
@CONF[:USE_COLORIZE] = (nc = ENV['NO_COLOR']).nil? || nc.empty?
@CONF[:USE_AUTOCOMPLETE] = ENV.fetch("IRB_USE_AUTOCOMPLETE", "true") != "false"
@CONF[:COMPLETOR] = :regexp
@CONF[:INSPECT_MODE] = true
@CONF[:USE_TRACER] = false
@CONF[:USE_LOADER] = false

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

@ -193,6 +193,10 @@ module IRB
}
end
def completion_info
'RegexpCompletor'
end
# Reads the next line from this input method.
#
# See IO#gets for more information.
@ -230,13 +234,13 @@ module IRB
HISTORY = Reline::HISTORY
include HistorySavingAbility
# Creates a new input method object using Reline
def initialize
def initialize(completor)
IRB.__send__(:set_encoding, Reline.encoding_system_needs.name, override: false)
super
super()
@eof = false
@completor = RegexpCompletor.new
@completor = completor
Reline.basic_word_break_characters = BASIC_WORD_BREAK_CHARACTERS
Reline.completion_append_character = nil
@ -270,6 +274,11 @@ module IRB
end
end
def completion_info
autocomplete_message = Reline.autocompletion ? 'Autocomplete' : 'Tab Complete'
"#{autocomplete_message}, #{@completor.inspect}"
end
def check_termination(&block)
@check_termination_proc = block
end

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

@ -0,0 +1,235 @@
# frozen_string_literal: true
require 'prism'
require 'irb/completion'
require_relative 'type_analyzer'
module IRB
module TypeCompletion
class Completor < BaseCompletor # :nodoc:
HIDDEN_METHODS = %w[Namespace TypeName] # defined by rbs, should be hidden
class << self
attr_accessor :last_completion_error
end
def inspect
name = 'TypeCompletion::Completor'
prism_info = "Prism: #{Prism::VERSION}"
if Types.rbs_builder
"#{name}(#{prism_info}, RBS: #{RBS::VERSION})"
elsif Types.rbs_load_error
"#{name}(#{prism_info}, RBS: #{Types.rbs_load_error.inspect})"
else
"#{name}(#{prism_info}, RBS: loading)"
end
end
def completion_candidates(preposing, target, _postposing, bind:)
@preposing = preposing
verbose, $VERBOSE = $VERBOSE, nil
code = "#{preposing}#{target}"
@result = analyze code, bind
name, candidates = candidates_from_result(@result)
all_symbols_pattern = /\A[ -\/:-@\[-`\{-~]*\z/
candidates.map(&:to_s).select { !_1.match?(all_symbols_pattern) && _1.start_with?(name) }.uniq.sort.map do
target + _1[name.size..]
end
rescue SyntaxError, StandardError => e
Completor.last_completion_error = e
handle_error(e)
[]
ensure
$VERBOSE = verbose
end
def doc_namespace(preposing, matched, postposing, bind:)
name = matched[/[a-zA-Z_0-9]*[!?=]?\z/]
method_doc = -> type do
type = type.types.find { _1.all_methods.include? name.to_sym }
case type
when Types::SingletonType
"#{Types.class_name_of(type.module_or_class)}.#{name}"
when Types::InstanceType
"#{Types.class_name_of(type.klass)}##{name}"
end
end
call_or_const_doc = -> type do
if name =~ /\A[A-Z]/
type = type.types.grep(Types::SingletonType).find { _1.module_or_class.const_defined?(name) }
type.module_or_class == Object ? name : "#{Types.class_name_of(type.module_or_class)}::#{name}" if type
else
method_doc.call(type)
end
end
value_doc = -> type do
return unless type
type.types.each do |t|
case t
when Types::SingletonType
return Types.class_name_of(t.module_or_class)
when Types::InstanceType
return Types.class_name_of(t.klass)
end
end
nil
end
case @result
in [:call_or_const, type, _name, _self_call]
call_or_const_doc.call type
in [:const, type, _name, scope]
if type
call_or_const_doc.call type
else
value_doc.call scope[name]
end
in [:gvar, _name, scope]
value_doc.call scope["$#{name}"]
in [:ivar, _name, scope]
value_doc.call scope["@#{name}"]
in [:cvar, _name, scope]
value_doc.call scope["@@#{name}"]
in [:call, type, _name, _self_call]
method_doc.call type
in [:lvar_or_method, _name, scope]
if scope.local_variables.include?(name)
value_doc.call scope[name]
else
method_doc.call scope.self_type
end
else
end
end
def candidates_from_result(result)
candidates = case result
in [:require, name]
retrieve_files_to_require_from_load_path
in [:require_relative, name]
retrieve_files_to_require_relative_from_current_dir
in [:call_or_const, type, name, self_call]
((self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS) | type.constants
in [:const, type, name, scope]
if type
scope_constants = type.types.flat_map do |t|
scope.table_module_constants(t.module_or_class) if t.is_a?(Types::SingletonType)
end
(scope_constants.compact | type.constants.map(&:to_s)).sort
else
scope.constants.sort | ReservedWords
end
in [:ivar, name, scope]
ivars = scope.instance_variables.sort
name == '@' ? ivars + scope.class_variables.sort : ivars
in [:cvar, name, scope]
scope.class_variables
in [:gvar, name, scope]
scope.global_variables
in [:symbol, name]
Symbol.all_symbols.map { _1.inspect[1..] }
in [:call, type, name, self_call]
(self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS
in [:lvar_or_method, name, scope]
scope.self_type.all_methods.map(&:to_s) | scope.local_variables | ReservedWords
else
[]
end
[name || '', candidates]
end
def analyze(code, binding = Object::TOPLEVEL_BINDING)
# Workaround for https://github.com/ruby/prism/issues/1592
return if code.match?(/%[qQ]\z/)
ast = Prism.parse(code, scopes: [binding.local_variables]).value
name = code[/(@@|@|\$)?\w*[!?=]?\z/]
*parents, target_node = find_target ast, code.bytesize - name.bytesize
return unless target_node
calculate_scope = -> { TypeAnalyzer.calculate_target_type_scope(binding, parents, target_node).last }
calculate_type_scope = ->(node) { TypeAnalyzer.calculate_target_type_scope binding, [*parents, target_node], node }
case target_node
when Prism::StringNode, Prism::InterpolatedStringNode
call_node, args_node = parents.last(2)
return unless call_node.is_a?(Prism::CallNode) && call_node.receiver.nil?
return unless args_node.is_a?(Prism::ArgumentsNode) && args_node.arguments.size == 1
case call_node.name
when :require
[:require, name.rstrip]
when :require_relative
[:require_relative, name.rstrip]
end
when Prism::SymbolNode
if parents.last.is_a? Prism::BlockArgumentNode # method(&:target)
receiver_type, _scope = calculate_type_scope.call target_node
[:call, receiver_type, name, false]
else
[:symbol, name] unless name.empty?
end
when Prism::CallNode
return [:lvar_or_method, name, calculate_scope.call] if target_node.receiver.nil?
self_call = target_node.receiver.is_a? Prism::SelfNode
op = target_node.call_operator
receiver_type, _scope = calculate_type_scope.call target_node.receiver
receiver_type = receiver_type.nonnillable if op == '&.'
[op == '::' ? :call_or_const : :call, receiver_type, name, self_call]
when Prism::LocalVariableReadNode, Prism::LocalVariableTargetNode
[:lvar_or_method, name, calculate_scope.call]
when Prism::ConstantReadNode, Prism::ConstantTargetNode
if parents.last.is_a? Prism::ConstantPathNode
path_node = parents.last
if path_node.parent # A::B
receiver, scope = calculate_type_scope.call(path_node.parent)
[:const, receiver, name, scope]
else # ::A
scope = calculate_scope.call
[:const, Types::SingletonType.new(Object), name, scope]
end
else
[:const, nil, name, calculate_scope.call]
end
when Prism::GlobalVariableReadNode, Prism::GlobalVariableTargetNode
[:gvar, name, calculate_scope.call]
when Prism::InstanceVariableReadNode, Prism::InstanceVariableTargetNode
[:ivar, name, calculate_scope.call]
when Prism::ClassVariableReadNode, Prism::ClassVariableTargetNode
[:cvar, name, calculate_scope.call]
end
end
def find_target(node, position)
location = (
case node
when Prism::CallNode
node.message_loc
when Prism::SymbolNode
node.value_loc
when Prism::StringNode
node.content_loc
when Prism::InterpolatedStringNode
node.closing_loc if node.parts.empty?
end
)
return [node] if location&.start_offset == position
node.compact_child_nodes.each do |n|
match = find_target(n, position)
next unless match
match.unshift node
return match
end
[node] if node.location.start_offset == position
end
def handle_error(e)
end
end
end
end

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

@ -0,0 +1,13 @@
# frozen_string_literal: true
module IRB
module TypeCompletion
module Methods
OBJECT_SINGLETON_CLASS_METHOD = Object.instance_method(:singleton_class)
OBJECT_INSTANCE_VARIABLES_METHOD = Object.instance_method(:instance_variables)
OBJECT_INSTANCE_VARIABLE_GET_METHOD = Object.instance_method(:instance_variable_get)
OBJECT_CLASS_METHOD = Object.instance_method(:class)
MODULE_NAME_METHOD = Module.instance_method(:name)
end
end
end

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

@ -0,0 +1,412 @@
# frozen_string_literal: true
require 'set'
require_relative 'types'
module IRB
module TypeCompletion
class RootScope
attr_reader :module_nesting, :self_object
def initialize(binding, self_object, local_variables)
@binding = binding
@self_object = self_object
@cache = {}
modules = [*binding.eval('::Module.nesting'), Object]
@module_nesting = modules.map { [_1, []] }
binding_local_variables = binding.local_variables
uninitialized_locals = local_variables - binding_local_variables
uninitialized_locals.each { @cache[_1] = Types::NIL }
@local_variables = (local_variables | binding_local_variables).map(&:to_s).to_set
@global_variables = Kernel.global_variables.map(&:to_s).to_set
@owned_constants_cache = {}
end
def level() = 0
def level_of(_name, _var_type) = 0
def mutable?() = false
def module_own_constant?(mod, name)
set = (@owned_constants_cache[mod] ||= Set.new(mod.constants.map(&:to_s)))
set.include? name
end
def get_const(nesting, path, _key = nil)
return unless nesting
result = path.reduce nesting do |mod, name|
return nil unless mod.is_a?(Module) && module_own_constant?(mod, name)
mod.const_get name
end
Types.type_from_object result
end
def get_cvar(nesting, path, name, _key = nil)
return Types::NIL unless nesting
result = path.reduce nesting do |mod, n|
return Types::NIL unless mod.is_a?(Module) && module_own_constant?(mod, n)
mod.const_get n
end
value = result.class_variable_get name if result.is_a?(Module) && name.size >= 3 && result.class_variable_defined?(name)
Types.type_from_object value
end
def [](name)
@cache[name] ||= (
value = case RootScope.type_by_name name
when :ivar
begin
Methods::OBJECT_INSTANCE_VARIABLE_GET_METHOD.bind_call(@self_object, name)
rescue NameError
end
when :lvar
begin
@binding.local_variable_get(name)
rescue NameError
end
when :gvar
@binding.eval name if @global_variables.include? name
end
Types.type_from_object(value)
)
end
def self_type
Types.type_from_object @self_object
end
def local_variables() = @local_variables.to_a
def global_variables() = @global_variables.to_a
def self.type_by_name(name)
if name.start_with? '@@'
# "@@cvar" or "@@cvar::[module_id]::[module_path]"
:cvar
elsif name.start_with? '@'
:ivar
elsif name.start_with? '$'
:gvar
elsif name.start_with? '%'
:internal
elsif name[0].downcase != name[0] || name[0].match?(/\d/)
# "ConstName" or "[module_id]::[const_path]"
:const
else
:lvar
end
end
end
class Scope
BREAK_RESULT = '%break'
NEXT_RESULT = '%next'
RETURN_RESULT = '%return'
PATTERNMATCH_BREAK = '%match'
attr_reader :parent, :mergeable_changes, :level, :module_nesting
def self.from_binding(binding, locals) = new(RootScope.new(binding, binding.receiver, locals))
def initialize(parent, table = {}, trace_ivar: true, trace_lvar: true, self_type: nil, nesting: nil)
@parent = parent
@level = parent.level + 1
@trace_ivar = trace_ivar
@trace_lvar = trace_lvar
@module_nesting = nesting ? [nesting, *parent.module_nesting] : parent.module_nesting
@self_type = self_type
@terminated = false
@jump_branches = []
@mergeable_changes = @table = table.transform_values { [level, _1] }
end
def mutable? = true
def terminated?
@terminated
end
def terminate_with(type, value)
return if terminated?
store_jump type, value, @mergeable_changes
terminate
end
def store_jump(type, value, changes)
return if terminated?
if has_own?(type)
changes[type] = [level, value]
@jump_branches << changes
elsif @parent.mutable?
@parent.store_jump(type, value, changes)
end
end
def terminate
return if terminated?
@terminated = true
@table = @mergeable_changes.dup
end
def trace?(name)
return false unless @parent
type = RootScope.type_by_name(name)
type == :ivar ? @trace_ivar : type == :lvar ? @trace_lvar : true
end
def level_of(name, var_type)
case var_type
when :ivar
return level unless @trace_ivar
when :gvar
return 0
end
variable_level, = @table[name]
variable_level || parent.level_of(name, var_type)
end
def get_const(nesting, path, key = nil)
key ||= [nesting.__id__, path].join('::')
_l, value = @table[key]
value || @parent.get_const(nesting, path, key)
end
def get_cvar(nesting, path, name, key = nil)
key ||= [name, nesting.__id__, path].join('::')
_l, value = @table[key]
value || @parent.get_cvar(nesting, path, name, key)
end
def [](name)
type = RootScope.type_by_name(name)
if type == :const
return get_const(nil, nil, name) || Types::NIL if name.include?('::')
module_nesting.each do |(nesting, path)|
value = get_const nesting, [*path, name]
return value if value
end
return Types::NIL
elsif type == :cvar
return get_cvar(nil, nil, nil, name) if name.include?('::')
nesting, path = module_nesting.first
return get_cvar(nesting, path, name)
end
level, value = @table[name]
if level
value
elsif trace? name
@parent[name]
elsif type == :ivar
self_instance_variable_get name
end
end
def set_const(nesting, path, value)
key = [nesting.__id__, path].join('::')
@table[key] = [0, value]
end
def set_cvar(nesting, path, name, value)
key = [name, nesting.__id__, path].join('::')
@table[key] = [0, value]
end
def []=(name, value)
type = RootScope.type_by_name(name)
if type == :const
if name.include?('::')
@table[name] = [0, value]
else
parent_module, parent_path = module_nesting.first
set_const parent_module, [*parent_path, name], value
end
return
elsif type == :cvar
if name.include?('::')
@table[name] = [0, value]
else
parent_module, parent_path = module_nesting.first
set_cvar parent_module, parent_path, name, value
end
return
end
variable_level = level_of name, type
@table[name] = [variable_level, value] if variable_level
end
def self_type
@self_type || @parent.self_type
end
def global_variables
gvar_keys = @table.keys.select do |name|
RootScope.type_by_name(name) == :gvar
end
gvar_keys | @parent.global_variables
end
def local_variables
lvar_keys = @table.keys.select do |name|
RootScope.type_by_name(name) == :lvar
end
lvar_keys |= @parent.local_variables if @trace_lvar
lvar_keys
end
def table_constants
constants = module_nesting.flat_map do |mod, path|
prefix = [mod.__id__, *path].join('::') + '::'
@table.keys.select { _1.start_with? prefix }.map { _1.delete_prefix(prefix).split('::').first }
end.uniq
constants |= @parent.table_constants if @parent.mutable?
constants
end
def table_module_constants(mod)
prefix = "#{mod.__id__}::"
constants = @table.keys.select { _1.start_with? prefix }.map { _1.delete_prefix(prefix).split('::').first }
constants |= @parent.table_constants if @parent.mutable?
constants
end
def base_scope
@parent.mutable? ? @parent.base_scope : @parent
end
def table_instance_variables
ivars = @table.keys.select { RootScope.type_by_name(_1) == :ivar }
ivars |= @parent.table_instance_variables if @parent.mutable? && @trace_ivar
ivars
end
def instance_variables
self_singleton_types = self_type.types.grep(Types::SingletonType)
singleton_classes = self_type.types.grep(Types::InstanceType).map(&:klass).select(&:singleton_class?)
base_self = base_scope.self_object
self_instance_variables = singleton_classes.flat_map do |singleton_class|
if singleton_class.respond_to? :attached_object
Methods::OBJECT_INSTANCE_VARIABLES_METHOD.bind_call(singleton_class.attached_object).map(&:to_s)
elsif singleton_class == Methods::OBJECT_SINGLETON_CLASS_METHOD.bind_call(base_self)
Methods::OBJECT_INSTANCE_VARIABLES_METHOD.bind_call(base_self).map(&:to_s)
else
[]
end
end
[
self_singleton_types.flat_map { _1.module_or_class.instance_variables.map(&:to_s) },
self_instance_variables || [],
table_instance_variables
].inject(:|)
end
def self_instance_variable_get(name)
self_objects = self_type.types.grep(Types::SingletonType).map(&:module_or_class)
singleton_classes = self_type.types.grep(Types::InstanceType).map(&:klass).select(&:singleton_class?)
base_self = base_scope.self_object
singleton_classes.each do |singleton_class|
if singleton_class.respond_to? :attached_object
self_objects << singleton_class.attached_object
elsif singleton_class == base_self.singleton_class
self_objects << base_self
end
end
types = self_objects.map do |object|
value = begin
Methods::OBJECT_INSTANCE_VARIABLE_GET_METHOD.bind_call(object, name)
rescue NameError
end
Types.type_from_object value
end
Types::UnionType[*types]
end
def table_class_variables
cvars = @table.keys.filter_map { _1.split('::', 2).first if RootScope.type_by_name(_1) == :cvar }
cvars |= @parent.table_class_variables if @parent.mutable?
cvars
end
def class_variables
cvars = table_class_variables
m, = module_nesting.first
cvars |= m.class_variables.map(&:to_s) if m.is_a? Module
cvars
end
def constants
module_nesting.flat_map do |nest,|
nest.constants
end.map(&:to_s) | table_constants
end
def merge_jumps
if terminated?
@terminated = false
@table = @mergeable_changes
merge @jump_branches
@terminated = true
else
merge [*@jump_branches, {}]
end
end
def conditional(&block)
run_branches(block, ->(_s) {}).first || Types::NIL
end
def never(&block)
block.call Scope.new(self, { BREAK_RESULT => nil, NEXT_RESULT => nil, PATTERNMATCH_BREAK => nil, RETURN_RESULT => nil })
end
def run_branches(*blocks)
results = []
branches = []
blocks.each do |block|
scope = Scope.new self
result = block.call scope
next if scope.terminated?
results << result
branches << scope.mergeable_changes
end
terminate if branches.empty?
merge branches
results
end
def has_own?(name)
@table.key? name
end
def update(child_scope)
current_level = level
child_scope.mergeable_changes.each do |name, (level, value)|
self[name] = value if level <= current_level
end
end
protected
def merge(branches)
current_level = level
merge = {}
branches.each do |changes|
changes.each do |name, (level, value)|
next if current_level < level
(merge[name] ||= []) << value
end
end
merge.each do |name, values|
values << self[name] unless values.size == branches.size
values.compact!
self[name] = Types::UnionType[*values.compact] unless values.empty?
end
end
end
end
end

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -0,0 +1,426 @@
# frozen_string_literal: true
require_relative 'methods'
module IRB
module TypeCompletion
module Types
OBJECT_TO_TYPE_SAMPLE_SIZE = 50
singleton_class.attr_reader :rbs_builder, :rbs_load_error
def self.preload_in_thread
return if @preload_started
@preload_started = true
Thread.new do
load_rbs_builder
end
end
def self.load_rbs_builder
require 'rbs'
require 'rbs/cli'
loader = RBS::CLI::LibraryOptions.new.loader
loader.add path: Pathname('sig')
@rbs_builder = RBS::DefinitionBuilder.new env: RBS::Environment.from_loader(loader).resolve_type_names
rescue LoadError, StandardError => e
@rbs_load_error = e
nil
end
def self.class_name_of(klass)
klass = klass.superclass if klass.singleton_class?
Methods::MODULE_NAME_METHOD.bind_call klass
end
def self.rbs_search_method(klass, method_name, singleton)
klass.ancestors.each do |ancestor|
name = class_name_of ancestor
next unless name && rbs_builder
type_name = RBS::TypeName(name).absolute!
definition = (singleton ? rbs_builder.build_singleton(type_name) : rbs_builder.build_instance(type_name)) rescue nil
method = definition.methods[method_name] if definition
return method if method
end
nil
end
def self.method_return_type(type, method_name)
receivers = type.types.map do |t|
case t
in SingletonType
[t, t.module_or_class, true]
in InstanceType
[t, t.klass, false]
end
end
types = receivers.flat_map do |receiver_type, klass, singleton|
method = rbs_search_method klass, method_name, singleton
next [] unless method
method.method_types.map do |method|
from_rbs_type(method.type.return_type, receiver_type, {})
end
end
UnionType[*types]
end
def self.rbs_methods(type, method_name, args_types, kwargs_type, has_block)
return [] unless rbs_builder
receivers = type.types.map do |t|
case t
in SingletonType
[t, t.module_or_class, true]
in InstanceType
[t, t.klass, false]
end
end
has_splat = args_types.include?(nil)
methods_with_score = receivers.flat_map do |receiver_type, klass, singleton|
method = rbs_search_method klass, method_name, singleton
next [] unless method
method.method_types.map do |method_type|
score = 0
score += 2 if !!method_type.block == has_block
reqs = method_type.type.required_positionals
opts = method_type.type.optional_positionals
rest = method_type.type.rest_positionals
trailings = method_type.type.trailing_positionals
keyreqs = method_type.type.required_keywords
keyopts = method_type.type.optional_keywords
keyrest = method_type.type.rest_keywords
args = args_types
if kwargs_type&.any? && keyreqs.empty? && keyopts.empty? && keyrest.nil?
kw_value_type = UnionType[*kwargs_type.values]
args += [InstanceType.new(Hash, K: SYMBOL, V: kw_value_type)]
end
if has_splat
score += 1 if args.count(&:itself) <= reqs.size + opts.size + trailings.size
elsif reqs.size + trailings.size <= args.size && (rest || args.size <= reqs.size + opts.size + trailings.size)
score += 2
centers = args[reqs.size...-trailings.size]
given = args.first(reqs.size) + centers.take(opts.size) + args.last(trailings.size)
expected = (reqs + opts.take(centers.size) + trailings).map(&:type)
if rest
given << UnionType[*centers.drop(opts.size)]
expected << rest.type
end
if given.any?
score += given.zip(expected).count do |t, e|
e = from_rbs_type e, receiver_type
intersect?(t, e) || (intersect?(STRING, e) && t.methods.include?(:to_str)) || (intersect?(INTEGER, e) && t.methods.include?(:to_int)) || (intersect?(ARRAY, e) && t.methods.include?(:to_ary))
end.fdiv(given.size)
end
end
[[method_type, given || [], expected || []], score]
end
end
max_score = methods_with_score.map(&:last).max
methods_with_score.select { _2 == max_score }.map(&:first)
end
def self.intersect?(a, b)
atypes = a.types.group_by(&:class)
btypes = b.types.group_by(&:class)
if atypes[SingletonType] && btypes[SingletonType]
aa, bb = [atypes, btypes].map {|types| types[SingletonType].map(&:module_or_class) }
return true if (aa & bb).any?
end
aa, bb = [atypes, btypes].map {|types| (types[InstanceType] || []).map(&:klass) }
(aa.flat_map(&:ancestors) & bb).any?
end
def self.type_from_object(object)
case object
when Array
InstanceType.new Array, { Elem: union_type_from_objects(object) }
when Hash
InstanceType.new Hash, { K: union_type_from_objects(object.keys), V: union_type_from_objects(object.values) }
when Module
SingletonType.new object
else
klass = Methods::OBJECT_SINGLETON_CLASS_METHOD.bind_call(object) rescue Methods::OBJECT_CLASS_METHOD.bind_call(object)
InstanceType.new klass
end
end
def self.union_type_from_objects(objects)
values = objects.size <= OBJECT_TO_TYPE_SAMPLE_SIZE ? objects : objects.sample(OBJECT_TO_TYPE_SAMPLE_SIZE)
klasses = values.map { Methods::OBJECT_CLASS_METHOD.bind_call(_1) }
UnionType[*klasses.uniq.map { InstanceType.new _1 }]
end
class SingletonType
attr_reader :module_or_class
def initialize(module_or_class)
@module_or_class = module_or_class
end
def transform() = yield(self)
def methods() = @module_or_class.methods
def all_methods() = methods | Kernel.methods
def constants() = @module_or_class.constants
def types() = [self]
def nillable?() = false
def nonnillable() = self
def inspect
"#{module_or_class}.itself"
end
end
class InstanceType
attr_reader :klass, :params
def initialize(klass, params = {})
@klass = klass
@params = params
end
def transform() = yield(self)
def methods() = rbs_methods.select { _2.public? }.keys | @klass.instance_methods
def all_methods() = rbs_methods.keys | @klass.instance_methods | @klass.private_instance_methods
def constants() = []
def types() = [self]
def nillable?() = (@klass == NilClass)
def nonnillable() = self
def rbs_methods
name = Types.class_name_of(@klass)
return {} unless name && Types.rbs_builder
type_name = RBS::TypeName(name).absolute!
Types.rbs_builder.build_instance(type_name).methods rescue {}
end
def inspect
if params.empty?
inspect_without_params
else
params_string = "[#{params.map { "#{_1}: #{_2.inspect}" }.join(', ')}]"
"#{inspect_without_params}#{params_string}"
end
end
def inspect_without_params
if klass == NilClass
'nil'
elsif klass == TrueClass
'true'
elsif klass == FalseClass
'false'
else
klass.singleton_class? ? klass.superclass.to_s : klass.to_s
end
end
end
NIL = InstanceType.new NilClass
OBJECT = InstanceType.new Object
TRUE = InstanceType.new TrueClass
FALSE = InstanceType.new FalseClass
SYMBOL = InstanceType.new Symbol
STRING = InstanceType.new String
INTEGER = InstanceType.new Integer
RANGE = InstanceType.new Range
REGEXP = InstanceType.new Regexp
FLOAT = InstanceType.new Float
RATIONAL = InstanceType.new Rational
COMPLEX = InstanceType.new Complex
ARRAY = InstanceType.new Array
HASH = InstanceType.new Hash
CLASS = InstanceType.new Class
MODULE = InstanceType.new Module
PROC = InstanceType.new Proc
class UnionType
attr_reader :types
def initialize(*types)
@types = []
singletons = []
instances = {}
collect = -> type do
case type
in UnionType
type.types.each(&collect)
in InstanceType
params = (instances[type.klass] ||= {})
type.params.each do |k, v|
(params[k] ||= []) << v
end
in SingletonType
singletons << type
end
end
types.each(&collect)
@types = singletons.uniq + instances.map do |klass, params|
InstanceType.new(klass, params.transform_values { |v| UnionType[*v] })
end
end
def transform(&block)
UnionType[*types.map(&block)]
end
def nillable?
types.any?(&:nillable?)
end
def nonnillable
UnionType[*types.reject { _1.is_a?(InstanceType) && _1.klass == NilClass }]
end
def self.[](*types)
type = new(*types)
if type.types.empty?
OBJECT
elsif type.types.size == 1
type.types.first
else
type
end
end
def methods() = @types.flat_map(&:methods).uniq
def all_methods() = @types.flat_map(&:all_methods).uniq
def constants() = @types.flat_map(&:constants).uniq
def inspect() = @types.map(&:inspect).join(' | ')
end
BOOLEAN = UnionType[TRUE, FALSE]
def self.array_of(*types)
type = types.size >= 2 ? UnionType[*types] : types.first || OBJECT
InstanceType.new Array, Elem: type
end
def self.from_rbs_type(return_type, self_type, extra_vars = {})
case return_type
when RBS::Types::Bases::Self
self_type
when RBS::Types::Bases::Bottom, RBS::Types::Bases::Nil
NIL
when RBS::Types::Bases::Any, RBS::Types::Bases::Void
OBJECT
when RBS::Types::Bases::Class
self_type.transform do |type|
case type
in SingletonType
InstanceType.new(self_type.module_or_class.is_a?(Class) ? Class : Module)
in InstanceType
SingletonType.new type.klass
end
end
UnionType[*types]
when RBS::Types::Bases::Bool
BOOLEAN
when RBS::Types::Bases::Instance
self_type.transform do |type|
if type.is_a?(SingletonType) && type.module_or_class.is_a?(Class)
InstanceType.new type.module_or_class
else
OBJECT
end
end
when RBS::Types::Union
UnionType[*return_type.types.map { from_rbs_type _1, self_type, extra_vars }]
when RBS::Types::Proc
PROC
when RBS::Types::Tuple
elem = UnionType[*return_type.types.map { from_rbs_type _1, self_type, extra_vars }]
InstanceType.new Array, Elem: elem
when RBS::Types::Record
InstanceType.new Hash, K: SYMBOL, V: OBJECT
when RBS::Types::Literal
InstanceType.new return_type.literal.class
when RBS::Types::Variable
if extra_vars.key? return_type.name
extra_vars[return_type.name]
elsif self_type.is_a? InstanceType
self_type.params[return_type.name] || OBJECT
elsif self_type.is_a? UnionType
types = self_type.types.filter_map do |t|
t.params[return_type.name] if t.is_a? InstanceType
end
UnionType[*types]
else
OBJECT
end
when RBS::Types::Optional
UnionType[from_rbs_type(return_type.type, self_type, extra_vars), NIL]
when RBS::Types::Alias
case return_type.name.name
when :int
INTEGER
when :boolish
BOOLEAN
when :string
STRING
else
# TODO: ???
OBJECT
end
when RBS::Types::Interface
# unimplemented
OBJECT
when RBS::Types::ClassInstance
klass = return_type.name.to_namespace.path.reduce(Object) { _1.const_get _2 }
if return_type.args
args = return_type.args.map { from_rbs_type _1, self_type, extra_vars }
names = rbs_builder.build_singleton(return_type.name).type_params
params = names.map.with_index { [_1, args[_2] || OBJECT] }.to_h
end
InstanceType.new klass, params || {}
end
end
def self.method_return_bottom?(method)
method.type.return_type.is_a? RBS::Types::Bases::Bottom
end
def self.match_free_variables(vars, types, values)
accumulator = {}
types.zip values do |t, v|
_match_free_variable(vars, t, v, accumulator) if v
end
accumulator.transform_values { UnionType[*_1] }
end
def self._match_free_variable(vars, rbs_type, value, accumulator)
case [rbs_type, value]
in [RBS::Types::Variable,]
(accumulator[rbs_type.name] ||= []) << value if vars.include? rbs_type.name
in [RBS::Types::ClassInstance, InstanceType]
names = rbs_builder.build_singleton(rbs_type.name).type_params
names.zip(rbs_type.args).each do |name, arg|
v = value.params[name]
_match_free_variable vars, arg, v, accumulator if v
end
in [RBS::Types::Tuple, InstanceType] if value.klass == Array
v = value.params[:Elem]
rbs_type.types.each do |t|
_match_free_variable vars, t, v, accumulator
end
in [RBS::Types::Record, InstanceType] if value.klass == Hash
# TODO
in [RBS::Types::Interface,]
definition = rbs_builder.build_interface rbs_type.name
convert = {}
definition.type_params.zip(rbs_type.args).each do |from, arg|
convert[from] = arg.name if arg.is_a? RBS::Types::Variable
end
return if convert.empty?
ac = {}
definition.methods.each do |method_name, method|
return_type = method_return_type value, method_name
method.defs.each do |method_def|
interface_return_type = method_def.type.type.return_type
_match_free_variable convert, interface_return_type, return_type, ac
end
end
convert.each do |from, to|
values = ac[from]
(accumulator[to] ||= []).concat values if values
end
else
end
end
end
end
end

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

@ -90,6 +90,7 @@ module TestIRB
Ruby\sversion:\s.+\n
IRB\sversion:\sirb\s.+\n
InputMethod:\sAbstract\sInputMethod\n
Completion: .+\n
\.irbrc\spath:\s.+\n
RUBY_PLATFORM:\s.+\n
East\sAsian\sAmbiguous\sWidth:\s\d\n
@ -113,6 +114,7 @@ module TestIRB
Ruby\sversion:\s.+\n
IRB\sversion:\sirb\s.+\n
InputMethod:\sAbstract\sInputMethod\n
Completion: .+\n
\.irbrc\spath:\s.+\n
RUBY_PLATFORM:\s.+\n
East\sAsian\sAmbiguous\sWidth:\s\d\n
@ -139,6 +141,7 @@ module TestIRB
Ruby\sversion:\s.+\n
IRB\sversion:\sirb\s.+\n
InputMethod:\sAbstract\sInputMethod\n
Completion: .+\n
RUBY_PLATFORM:\s.+\n
East\sAsian\sAmbiguous\sWidth:\s\d\n
#{@is_win ? 'Code\spage:\s\d+\n' : ''}
@ -168,6 +171,7 @@ module TestIRB
Ruby\sversion:\s.+\n
IRB\sversion:\sirb\s.+\n
InputMethod:\sAbstract\sInputMethod\n
Completion: .+\n
RUBY_PLATFORM:\s.+\n
East\sAsian\sAmbiguous\sWidth:\s\d\n
#{@is_win ? 'Code\spage:\s\d+\n' : ''}
@ -196,6 +200,7 @@ module TestIRB
Ruby\sversion: .+\n
IRB\sversion:\sirb .+\n
InputMethod:\sAbstract\sInputMethod\n
Completion: .+\n
\.irbrc\spath: .+\n
RUBY_PLATFORM: .+\n
LANG\senv:\sja_JP\.UTF-8\n

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

@ -652,6 +652,24 @@ module TestIRB
], out)
end
def test_build_completor
verbose, $VERBOSE = $VERBOSE, nil
original_completor = IRB.conf[:COMPLETOR]
IRB.conf[:COMPLETOR] = :regexp
assert_equal 'IRB::RegexpCompletor', @context.send(:build_completor).class.name
IRB.conf[:COMPLETOR] = :type
if RUBY_VERSION >= '3.0.0' && RUBY_ENGINE != 'truffleruby'
assert_equal 'IRB::TypeCompletion::Completor', @context.send(:build_completor).class.name
else
assert_equal 'IRB::RegexpCompletor', @context.send(:build_completor).class.name
end
IRB.conf[:COMPLETOR] = :unknown
assert_equal 'IRB::RegexpCompletor', @context.send(:build_completor).class.name
ensure
$VERBOSE = verbose
IRB.conf[:COMPLETOR] = original_completor
end
private
def without_colorize

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

@ -24,7 +24,7 @@ module TestIRB
def test_initialization
Reline.completion_proc = nil
Reline.dig_perfect_match_proc = nil
IRB::RelineInputMethod.new
IRB::RelineInputMethod.new(IRB::RegexpCompletor.new)
assert_nil Reline.completion_append_character
assert_equal '', Reline.completer_quote_characters
@ -40,7 +40,7 @@ module TestIRB
IRB.conf[:USE_AUTOCOMPLETE] = false
IRB::RelineInputMethod.new
IRB::RelineInputMethod.new(IRB::RegexpCompletor.new)
refute Reline.autocompletion
assert_equal empty_proc, Reline.dialog_proc(:show_doc).dialog_proc
@ -55,7 +55,7 @@ module TestIRB
IRB.conf[:USE_AUTOCOMPLETE] = true
IRB::RelineInputMethod.new
IRB::RelineInputMethod.new(IRB::RegexpCompletor.new)
assert Reline.autocompletion
assert_not_equal empty_proc, Reline.dialog_proc(:show_doc).dialog_proc
@ -71,7 +71,7 @@ module TestIRB
IRB.conf[:USE_AUTOCOMPLETE] = true
without_rdoc do
IRB::RelineInputMethod.new
IRB::RelineInputMethod.new(IRB::RegexpCompletor.new)
end
assert Reline.autocompletion
@ -89,7 +89,7 @@ module TestIRB
end
def display_document(target, bind)
input_method = IRB::RelineInputMethod.new
input_method = IRB::RelineInputMethod.new(IRB::RegexpCompletor.new)
input_method.instance_variable_set(:@completion_params, [target, '', '', bind])
input_method.display_document(target, driver: @driver)
end

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

@ -0,0 +1,112 @@
# frozen_string_literal: true
return unless RUBY_VERSION >= '3.0.0'
return if RUBY_ENGINE == 'truffleruby' # needs endless method definition
require 'irb/type_completion/scope'
require_relative '../helper'
module TestIRB
class TypeCompletionScopeTest < TestCase
A, B, C, D, E, F, G, H, I, J, K = ('A'..'K').map do |name|
klass = Class.new
klass.define_singleton_method(:inspect) { name }
IRB::TypeCompletion::Types::InstanceType.new(klass)
end
def assert_type(expected_types, type)
assert_equal [*expected_types].map(&:klass).to_set, type.types.map(&:klass).to_set
end
def table(*local_variable_names)
local_variable_names.to_h { [_1, IRB::TypeCompletion::Types::NIL] }
end
def base_scope
IRB::TypeCompletion::RootScope.new(binding, Object.new, [])
end
def test_lvar
scope = IRB::TypeCompletion::Scope.new base_scope, table('a')
scope['a'] = A
assert_equal A, scope['a']
end
def test_conditional
scope = IRB::TypeCompletion::Scope.new base_scope, table('a')
scope.conditional do |sub_scope|
sub_scope['a'] = A
end
assert_type [A, IRB::TypeCompletion::Types::NIL], scope['a']
end
def test_branch
scope = IRB::TypeCompletion::Scope.new base_scope, table('a', 'b', 'c', 'd')
scope['c'] = A
scope['d'] = B
scope.run_branches(
-> { _1['a'] = _1['c'] = _1['d'] = C },
-> { _1['a'] = _1['b'] = _1['d'] = D },
-> { _1['a'] = _1['b'] = _1['d'] = E },
-> { _1['a'] = _1['b'] = _1['c'] = F; _1.terminate }
)
assert_type [C, D, E], scope['a']
assert_type [IRB::TypeCompletion::Types::NIL, D, E], scope['b']
assert_type [A, C], scope['c']
assert_type [C, D, E], scope['d']
end
def test_scope_local_variables
scope1 = IRB::TypeCompletion::Scope.new base_scope, table('a', 'b')
scope2 = IRB::TypeCompletion::Scope.new scope1, table('b', 'c'), trace_lvar: false
scope3 = IRB::TypeCompletion::Scope.new scope2, table('c', 'd')
scope4 = IRB::TypeCompletion::Scope.new scope2, table('d', 'e')
assert_empty base_scope.local_variables
assert_equal %w[a b], scope1.local_variables.sort
assert_equal %w[b c], scope2.local_variables.sort
assert_equal %w[b c d], scope3.local_variables.sort
assert_equal %w[b c d e], scope4.local_variables.sort
end
def test_nested_scope
scope = IRB::TypeCompletion::Scope.new base_scope, table('a', 'b', 'c')
scope['a'] = A
scope['b'] = A
scope['c'] = A
sub_scope = IRB::TypeCompletion::Scope.new scope, { 'c' => B }
assert_type A, sub_scope['a']
assert_type A, sub_scope['b']
assert_type B, sub_scope['c']
sub_scope['a'] = C
sub_scope.conditional { _1['b'] = C }
sub_scope['c'] = C
assert_type C, sub_scope['a']
assert_type [A, C], sub_scope['b']
assert_type C, sub_scope['c']
scope.update sub_scope
assert_type C, scope['a']
assert_type [A, C], scope['b']
assert_type A, scope['c']
end
def test_break
scope = IRB::TypeCompletion::Scope.new base_scope, table('a')
scope['a'] = A
breakable_scope = IRB::TypeCompletion::Scope.new scope, { IRB::TypeCompletion::Scope::BREAK_RESULT => nil }
breakable_scope.conditional do |sub|
sub['a'] = B
assert_type [B], sub['a']
sub.terminate_with IRB::TypeCompletion::Scope::BREAK_RESULT, C
sub['a'] = C
assert_type [C], sub['a']
end
assert_type [A], breakable_scope['a']
breakable_scope[IRB::TypeCompletion::Scope::BREAK_RESULT] = D
breakable_scope.merge_jumps
assert_type [C, D], breakable_scope[IRB::TypeCompletion::Scope::BREAK_RESULT]
scope.update breakable_scope
assert_type [A, B], scope['a']
end
end
end

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

@ -0,0 +1,697 @@
# frozen_string_literal: true
# Run test only when Ruby >= 3.0 and %w[prism rbs] are available
return unless RUBY_VERSION >= '3.0.0'
return if RUBY_ENGINE == 'truffleruby' # needs endless method definition
begin
require 'prism'
require 'rbs'
rescue LoadError
return
end
require 'irb/completion'
require 'irb/type_completion/completor'
require_relative '../helper'
module TestIRB
class TypeCompletionAnalyzeTest < TestCase
def setup
IRB::TypeCompletion::Types.load_rbs_builder unless IRB::TypeCompletion::Types.rbs_builder
end
def empty_binding
binding
end
def analyze(code, binding: nil)
completor = IRB::TypeCompletion::Completor.new
def completor.handle_error(e)
raise e
end
completor.analyze(code, binding || empty_binding)
end
def assert_analyze_type(code, type, token = nil, binding: empty_binding)
result_type, result_token = analyze(code, binding: binding)
assert_equal type, result_type
assert_equal token, result_token if token
end
def assert_call(code, include: nil, exclude: nil, binding: nil)
raise ArgumentError if include.nil? && exclude.nil?
result = analyze(code.strip, binding: binding)
type = result[1] if result[0] == :call
klasses = type.types.flat_map do
_1.klass.singleton_class? ? [_1.klass.superclass, _1.klass] : _1.klass
end
assert ([*include] - klasses).empty?, "Expected #{klasses} to include #{include}" if include
assert (klasses & [*exclude]).empty?, "Expected #{klasses} not to include #{exclude}" if exclude
end
def test_lvar_ivar_gvar_cvar
assert_analyze_type('puts(x', :lvar_or_method, 'x')
assert_analyze_type('puts($', :gvar, '$')
assert_analyze_type('puts($x', :gvar, '$x')
assert_analyze_type('puts(@', :ivar, '@')
assert_analyze_type('puts(@x', :ivar, '@x')
assert_analyze_type('puts(@@', :cvar, '@@')
assert_analyze_type('puts(@@x', :cvar, '@@x')
end
def test_rescue
assert_call '(1 rescue 1.0).', include: [Integer, Float]
assert_call 'a=""; (a=1) rescue (a=1.0); a.', include: [Integer, Float], exclude: String
assert_call 'begin; 1; rescue; 1.0; end.', include: [Integer, Float]
assert_call 'begin; 1; rescue A; 1.0; rescue B; 1i; end.', include: [Integer, Float, Complex]
assert_call 'begin; 1i; rescue; 1.0; else; 1; end.', include: [Integer, Float], exclude: Complex
assert_call 'begin; 1; rescue; 1.0; ensure; 1i; end.', include: [Integer, Float], exclude: Complex
assert_call 'begin; 1i; rescue; 1.0; else; 1; ensure; 1i; end.', include: [Integer, Float], exclude: Complex
assert_call 'a=""; begin; a=1; rescue; a=1.0; end; a.', include: [Integer, Float], exclude: [String]
assert_call 'a=""; begin; a=1; rescue; a=1.0; else; a=1r; end; a.', include: [Float, Rational], exclude: [String, Integer]
assert_call 'a=""; begin; a=1; rescue; a=1.0; else; a=1r; ensure; a = 1i; end; a.', include: Complex, exclude: [Float, Rational, String, Integer]
end
def test_rescue_assign
assert_equal [:lvar_or_method, 'a'], analyze('begin; rescue => a')[0, 2]
assert_equal [:gvar, '$a'], analyze('begin; rescue => $a')[0, 2]
assert_equal [:ivar, '@a'], analyze('begin; rescue => @a')[0, 2]
assert_equal [:cvar, '@@a'], analyze('begin; rescue => @@a')[0, 2]
assert_equal [:const, 'A'], analyze('begin; rescue => A').values_at(0, 2)
assert_equal [:call, 'b'], analyze('begin; rescue => a.b').values_at(0, 2)
end
def test_ref
bind = eval <<~RUBY
class (Module.new)::A
@ivar = :a
@@cvar = 'a'
binding
end
RUBY
assert_call('STDIN.', include: STDIN.singleton_class)
assert_call('$stdin.', include: $stdin.singleton_class)
assert_call('@ivar.', include: Symbol, binding: bind)
assert_call('@@cvar.', include: String, binding: bind)
lbind = eval('lvar = 1; binding')
assert_call('lvar.', include: Integer, binding: lbind)
end
def test_self_ivar_ref
obj = Object.new
obj.instance_variable_set(:@hoge, 1)
assert_call('obj.instance_eval { @hoge.', include: Integer, binding: obj.instance_eval { binding })
if Class.method_defined? :attached_object
bind = binding
assert_call('obj.instance_eval { @hoge.', include: Integer, binding: bind)
assert_call('@hoge = 1.0; obj.instance_eval { @hoge.', include: Integer, exclude: Float, binding: bind)
assert_call('@hoge = 1.0; obj.instance_eval { @hoge = "" }; @hoge.', include: Float, exclude: [Integer, String], binding: bind)
assert_call('@fuga = 1.0; obj.instance_eval { @fuga.', exclude: Float, binding: bind)
assert_call('@fuga = 1.0; obj.instance_eval { @fuga = "" }; @fuga.', include: Float, exclude: [Integer, String], binding: bind)
end
end
class CVarModule
@@test_cvar = 1
end
def test_module_cvar_ref
bind = binding
assert_call('@@foo=1; class A; @@foo.', exclude: Integer, binding: bind)
assert_call('@@foo=1; class A; @@foo=1.0; @@foo.', include: Float, exclude: Integer, binding: bind)
assert_call('@@foo=1; class A; @@foo=1.0; end; @@foo.', include: Integer, exclude: Float, binding: bind)
assert_call('module CVarModule; @@test_cvar.', include: Integer, binding: bind)
assert_call('class Array; @@foo = 1; end; class Array; @@foo.', include: Integer, binding: bind)
assert_call('class Array; class B; @@foo = 1; end; class B; @@foo.', include: Integer, binding: bind)
assert_call('class Array; class B; @@foo = 1; end; @@foo.', exclude: Integer, binding: bind)
end
def test_lvar_singleton_method
a = 1
b = +''
c = Object.new
d = [a, b, c]
binding = Kernel.binding
assert_call('a.', include: Integer, exclude: String, binding: binding)
assert_call('b.', include: b.singleton_class, exclude: [Integer, Object], binding: binding)
assert_call('c.', include: c.singleton_class, exclude: [Integer, String], binding: binding)
assert_call('d.', include: d.class, exclude: [Integer, String, Object], binding: binding)
assert_call('d.sample.', include: [Integer, String, Object], exclude: [b.singleton_class, c.singleton_class], binding: binding)
end
def test_local_variable_assign
assert_call('(a = 1).', include: Integer)
assert_call('a = 1; a = ""; a.', include: String, exclude: Integer)
assert_call('1 => a; a.', include: Integer)
end
def test_block_symbol
assert_call('[1].map(&:', include: Integer)
assert_call('1.to_s.tap(&:', include: String)
end
def test_union_splat
assert_call('a, = [[:a], 1, nil].sample; a.', include: [Symbol, Integer, NilClass], exclude: Object)
assert_call('[[:a], 1, nil].each do _2; _1.', include: [Symbol, Integer, NilClass], exclude: Object)
assert_call('a = [[:a], 1, nil, ("a".."b")].sample; [*a].sample.', include: [Symbol, Integer, NilClass, String], exclude: Object)
end
def test_range
assert_call('(1..2).first.', include: Integer)
assert_call('("a".."b").first.', include: String)
assert_call('(..1.to_f).first.', include: Float)
assert_call('(1.to_s..).first.', include: String)
assert_call('(1..2.0).first.', include: [Float, Integer])
end
def test_conditional_assign
assert_call('a = 1; a = "" if cond; a.', include: [String, Integer], exclude: NilClass)
assert_call('a = 1 if cond; a.', include: [Integer, NilClass])
assert_call(<<~RUBY, include: [String, Symbol], exclude: [Integer, NilClass])
a = 1
cond ? a = '' : a = :a
a.
RUBY
end
def test_block
assert_call('nil.then{1}.', include: Integer, exclude: NilClass)
assert_call('nil.then(&:to_s).', include: String, exclude: NilClass)
end
def test_block_break
assert_call('1.tap{}.', include: [Integer], exclude: NilClass)
assert_call('1.tap{break :a}.', include: [Symbol, Integer], exclude: NilClass)
assert_call('1.tap{break :a, :b}[0].', include: Symbol)
assert_call('1.tap{break :a; break "a"}.', include: [Symbol, Integer], exclude: [NilClass, String])
assert_call('1.tap{break :a if b}.', include: [Symbol, Integer], exclude: NilClass)
assert_call('1.tap{break :a; break "a" if b}.', include: [Symbol, Integer], exclude: [NilClass, String])
assert_call('1.tap{if cond; break :a; else; break "a"; end}.', include: [Symbol, Integer, String], exclude: NilClass)
end
def test_instance_eval
assert_call('1.instance_eval{:a.then{self.', include: Integer, exclude: Symbol)
assert_call('1.then{:a.instance_eval{self.', include: Symbol, exclude: Integer)
end
def test_block_next
assert_call('nil.then{1}.', include: Integer, exclude: [NilClass, Object])
assert_call('nil.then{next 1}.', include: Integer, exclude: [NilClass, Object])
assert_call('nil.then{next :a, :b}[0].', include: Symbol)
assert_call('nil.then{next 1; 1.0}.', include: Integer, exclude: [Float, NilClass, Object])
assert_call('nil.then{next 1; next 1.0}.', include: Integer, exclude: [Float, NilClass, Object])
assert_call('nil.then{1 if cond}.', include: [Integer, NilClass], exclude: Object)
assert_call('nil.then{if cond; 1; else; 1.0; end}.', include: [Integer, Float], exclude: [NilClass, Object])
assert_call('nil.then{next 1 if cond; 1.0}.', include: [Integer, Float], exclude: [NilClass, Object])
assert_call('nil.then{if cond; next 1; else; next 1.0; end; "a"}.', include: [Integer, Float], exclude: [String, NilClass, Object])
assert_call('nil.then{if cond; next 1; else; next 1.0; end; next "a"}.', include: [Integer, Float], exclude: [String, NilClass, Object])
end
def test_vars_with_branch_termination
assert_call('a=1; tap{break; a=//}; a.', include: Integer, exclude: Regexp)
assert_call('a=1; tap{a=1.0; break; a=//}; a.', include: [Integer, Float], exclude: Regexp)
assert_call('a=1; tap{next; a=//}; a.', include: Integer, exclude: Regexp)
assert_call('a=1; tap{a=1.0; next; a=//}; a.', include: [Integer, Float], exclude: Regexp)
assert_call('a=1; while cond; break; a=//; end; a.', include: Integer, exclude: Regexp)
assert_call('a=1; while cond; a=1.0; break; a=//; end; a.', include: [Integer, Float], exclude: Regexp)
assert_call('a=1; ->{ break; a=// }; a.', include: Integer, exclude: Regexp)
assert_call('a=1; ->{ a=1.0; break; a=// }; a.', include: [Integer, Float], exclude: Regexp)
assert_call('a=1; tap{ break; a=// if cond }; a.', include: Integer, exclude: Regexp)
assert_call('a=1; tap{ next; a=// if cond }; a.', include: Integer, exclude: Regexp)
assert_call('a=1; while cond; break; a=// if cond; end; a.', include: Integer, exclude: Regexp)
assert_call('a=1; ->{ break; a=// if cond }; a.', include: Integer, exclude: Regexp)
assert_call('a=1; tap{if cond; a=:a; break; a=""; end; a.', include: Integer, exclude: [Symbol, String])
assert_call('a=1; tap{if cond; a=:a; break; a=""; end; a=//}; a.', include: [Integer, Symbol, Regexp], exclude: String)
assert_call('a=1; tap{if cond; a=:a; break; a=""; else; break; end; a=//}; a.', include: [Integer, Symbol], exclude: [String, Regexp])
assert_call('a=1; tap{if cond; a=:a; next; a=""; end; a.', include: Integer, exclude: [Symbol, String])
assert_call('a=1; tap{if cond; a=:a; next; a=""; end; a=//}; a.', include: [Integer, Symbol, Regexp], exclude: String)
assert_call('a=1; tap{if cond; a=:a; next; a=""; else; next; end; a=//}; a.', include: [Integer, Symbol], exclude: [String, Regexp])
assert_call('def f(a=1); if cond; a=:a; return; a=""; end; a.', include: Integer, exclude: [Symbol, String])
assert_call('a=1; while cond; if cond; a=:a; break; a=""; end; a.', include: Integer, exclude: [Symbol, String])
assert_call('a=1; while cond; if cond; a=:a; break; a=""; end; a=//; end; a.', include: [Integer, Symbol, Regexp], exclude: String)
assert_call('a=1; while cond; if cond; a=:a; break; a=""; else; break; end; a=//; end; a.', include: [Integer, Symbol], exclude: [String, Regexp])
assert_call('a=1; ->{ if cond; a=:a; break; a=""; end; a.', include: Integer, exclude: [Symbol, String])
assert_call('a=1; ->{ if cond; a=:a; break; a=""; end; a=// }; a.', include: [Integer, Symbol, Regexp], exclude: String)
assert_call('a=1; ->{ if cond; a=:a; break; a=""; else; break; end; a=// }; a.', include: [Integer, Symbol], exclude: [String, Regexp])
# continue evaluation on terminated branch
assert_call('a=1; tap{ a=1.0; break; a=// if cond; a.', include: [Regexp, Float], exclude: Integer)
assert_call('a=1; tap{ a=1.0; next; a=// if cond; a.', include: [Regexp, Float], exclude: Integer)
assert_call('a=1; ->{ a=1.0; break; a=// if cond; a.', include: [Regexp, Float], exclude: Integer)
assert_call('a=1; while cond; a=1.0; break; a=// if cond; a.', include: [Regexp, Float], exclude: Integer)
end
def test_to_str_to_int
sobj = Struct.new(:to_str).new('a')
iobj = Struct.new(:to_int).new(1)
binding = Kernel.binding
assert_equal String, ([] * sobj).class
assert_equal Array, ([] * iobj).class
assert_call('([]*sobj).', include: String, exclude: Array, binding: binding)
assert_call('([]*iobj).', include: Array, exclude: String, binding: binding)
end
def test_method_select
assert_call('([]*4).', include: Array, exclude: String)
assert_call('([]*"").', include: String, exclude: Array)
assert_call('([]*unknown).', include: [String, Array])
assert_call('p(1).', include: Integer)
assert_call('p(1, 2).', include: Array, exclude: Integer)
assert_call('2.times.', include: Enumerator, exclude: Integer)
assert_call('2.times{}.', include: Integer, exclude: Enumerator)
end
def test_interface_match_var
assert_call('([1]+[:a]+["a"]).sample.', include: [Integer, String, Symbol])
end
def test_lvar_scope
code = <<~RUBY
tap { a = :never }
a = 1 if x?
tap {|a| a = :never }
tap { a = 'maybe' }
a = {} if x?
a.
RUBY
assert_call(code, include: [Hash, Integer, String], exclude: [Symbol])
end
def test_lvar_scope_complex
assert_call('if cond; a = 1; else; tap { a = :a }; end; a.', include: [NilClass, Integer, Symbol], exclude: [Object])
assert_call('def f; if cond; a = 1; return; end; tap { a = :a }; a.', include: [NilClass, Symbol], exclude: [Integer, Object])
assert_call('def f; if cond; return; a = 1; end; tap { a = :a }; a.', include: [NilClass, Symbol], exclude: [Integer, Object])
assert_call('def f; if cond; return; if cond; return; a = 1; end; end; tap { a = :a }; a.', include: [NilClass, Symbol], exclude: [Integer, Object])
assert_call('def f; if cond; return; if cond; return; a = 1; end; end; tap { a = :a }; a.', include: [NilClass, Symbol], exclude: [Integer, Object])
end
def test_gvar_no_scope
code = <<~RUBY
tap { $a = :maybe }
$a = 'maybe' if x?
$a.
RUBY
assert_call(code, include: [Symbol, String])
end
def test_ivar_no_scope
code = <<~RUBY
tap { @a = :maybe }
@a = 'maybe' if x?
@a.
RUBY
assert_call(code, include: [Symbol, String])
end
def test_massign
assert_call('(a,=1).', include: Integer)
assert_call('(a,=[*1])[0].', include: Integer)
assert_call('(a,=[1,2])[0].', include: Integer)
assert_call('a,=[1,2]; a.', include: Integer, exclude: Array)
assert_call('a,b=[1,2]; a.', include: Integer, exclude: Array)
assert_call('a,b=[1,2]; b.', include: Integer, exclude: Array)
assert_call('a,*,b=[1,2]; a.', include: Integer, exclude: Array)
assert_call('a,*,b=[1,2]; b.', include: Integer, exclude: Array)
assert_call('a,*b=[1,2]; a.', include: Integer, exclude: Array)
assert_call('a,*b=[1,2]; b.', include: Array, exclude: Integer)
assert_call('a,*b=[1,2]; b.sample.', include: Integer)
assert_call('a,*,(*)=[1,2]; a.', include: Integer)
assert_call('*a=[1,2]; a.', include: Array, exclude: Integer)
assert_call('*a=[1,2]; a.sample.', include: Integer)
assert_call('a,*b,c=[1,2,3]; b.', include: Array, exclude: Integer)
assert_call('a,*b,c=[1,2,3]; b.sample.', include: Integer)
assert_call('a,b=(cond)?[1,2]:[:a,:b]; a.', include: [Integer, Symbol])
assert_call('a,b=(cond)?[1,2]:[:a,:b]; b.', include: [Integer, Symbol])
assert_call('a,b=(cond)?[1,2]:"s"; a.', include: [Integer, String])
assert_call('a,b=(cond)?[1,2]:"s"; b.', include: Integer, exclude: String)
assert_call('a,*b=(cond)?[1,2]:"s"; a.', include: [Integer, String])
assert_call('a,*b=(cond)?[1,2]:"s"; b.', include: Array, exclude: [Integer, String])
assert_call('a,*b=(cond)?[1,2]:"s"; b.sample.', include: Integer, exclude: String)
assert_call('*a=(cond)?[1,2]:"s"; a.', include: Array, exclude: [Integer, String])
assert_call('*a=(cond)?[1,2]:"s"; a.sample.', include: [Integer, String])
assert_call('a,(b,),c=[1,[:a],4]; b.', include: Symbol)
assert_call('a,(b,(c,))=1; a.', include: Integer)
assert_call('a,(b,(*c))=1; c.', include: Array)
assert_call('(a=1).b, c = 1; a.', include: Integer)
assert_call('a, ((b=1).c, d) = 1; b.', include: Integer)
assert_call('a, b[c=1] = 1; c.', include: Integer)
assert_call('a, b[*(c=1)] = 1; c.', include: Integer)
# incomplete massign
assert_analyze_type('a,b', :lvar_or_method, 'b')
assert_call('(a=1).b, a.', include: Integer)
assert_call('a=1; *a.', include: Integer)
end
def test_field_assign
assert_call('(a.!=1).', exclude: Integer)
assert_call('(a.b=1).', include: Integer, exclude: NilClass)
assert_call('(a&.b=1).', include: Integer)
assert_call('(nil&.b=1).', include: NilClass)
assert_call('(a[]=1).', include: Integer)
assert_call('(a[b]=1).', include: Integer)
assert_call('(a.[]=1).', exclude: Integer)
end
def test_def
assert_call('def f; end.', include: Symbol)
assert_call('s=""; def s.f; self.', include: String)
assert_call('def (a="").f; end; a.', include: String)
assert_call('def f(a=1); a.', include: Integer)
assert_call('def f(**nil); 1.', include: Integer)
assert_call('def f((*),*); 1.', include: Integer)
assert_call('def f(a,*b); b.', include: Array)
assert_call('def f(a,x:1); x.', include: Integer)
assert_call('def f(a,x:,**); 1.', include: Integer)
assert_call('def f(a,x:,**y); y.', include: Hash)
assert_call('def f((*a)); a.', include: Array)
assert_call('def f(a,b=1,*c,d,x:0,y:,**z,&e); e.arity.', include: Integer)
assert_call('def f(...); 1.', include: Integer)
assert_call('def f(a,...); 1.', include: Integer)
assert_call('def f(...); g(...); 1.', include: Integer)
assert_call('def f(*,**,&); g(*,**,&); 1.', include: Integer)
assert_call('def f(*,**,&); {**}.', include: Hash)
assert_call('def f(*,**,&); [*,**].', include: Array)
assert_call('class Array; def f; self.', include: Array)
end
def test_defined
assert_call('defined?(a.b+c).', include: [String, NilClass])
assert_call('defined?(a = 1); tap { a = 1.0 }; a.', include: [Integer, Float, NilClass])
end
def test_ternary_operator
assert_call('condition ? 1.chr.', include: [String])
assert_call('condition ? value : 1.chr.', include: [String])
assert_call('condition ? cond ? cond ? value : cond ? value : 1.chr.', include: [String])
end
def test_block_parameter
assert_call('method { |arg = 1.chr.', include: [String])
assert_call('method do |arg = 1.chr.', include: [String])
assert_call('method { |arg1 = 1.|(2|3), arg2 = 1.chr.', include: [String])
assert_call('method do |arg1 = 1.|(2|3), arg2 = 1.chr.', include: [String])
end
def test_self
integer_binding = 1.instance_eval { Kernel.binding }
assert_call('self.', include: [Integer], binding: integer_binding)
string = +''
string_binding = string.instance_eval { Kernel.binding }
assert_call('self.', include: [string.singleton_class], binding: string_binding)
object = Object.new
object.instance_eval { @int = 1; @string = string }
object_binding = object.instance_eval { Kernel.binding }
assert_call('self.', include: [object.singleton_class], binding: object_binding)
assert_call('@int.', include: [Integer], binding: object_binding)
assert_call('@string.', include: [String], binding: object_binding)
end
def test_optional_chain
assert_call('[1,nil].sample.', include: [Integer, NilClass])
assert_call('[1,nil].sample&.', include: [Integer], exclude: [NilClass])
assert_call('[1,nil].sample.chr.', include: [String], exclude: [NilClass])
assert_call('[1,nil].sample&.chr.', include: [String, NilClass])
assert_call('[1,nil].sample.chr&.ord.', include: [Integer], exclude: [NilClass])
assert_call('a = 1; b.c(a = :a); a.', include: [Symbol], exclude: [Integer])
assert_call('a = 1; b&.c(a = :a); a.', include: [Integer, Symbol])
end
def test_class_module
assert_call('class (1.', include: Integer)
assert_call('class (a=1)::B; end; a.', include: Integer)
assert_call('class Array; 1; end.', include: Integer)
assert_call('class ::Array; 1; end.', include: Integer)
assert_call('class Array::A; 1; end.', include: Integer)
assert_call('class Array; self.new.', include: Array)
assert_call('class ::Array; self.new.', include: Array)
assert_call('class Array::A; self.', include: Class)
assert_call('class (a=1)::A; end; a.', include: Integer)
assert_call('module M; 1; end.', include: Integer)
assert_call('module ::M; 1; end.', include: Integer)
assert_call('module Array::M; 1; end.', include: Integer)
assert_call('module M; self.', include: Module)
assert_call('module Array::M; self.', include: Module)
assert_call('module ::M; self.', include: Module)
assert_call('module (a=1)::M; end; a.', include: Integer)
assert_call('class << Array; 1; end.', include: Integer)
assert_call('class << a; 1; end.', include: Integer)
assert_call('a = ""; class << a; self.superclass.', include: Class)
end
def test_constant_path
assert_call('class A; X=1; class B; X=""; X.', include: String, exclude: Integer)
assert_call('class A; X=1; class B; X=""; end; X.', include: Integer, exclude: String)
assert_call('class A; class B; X=1; end; end; class A; class B; X.', include: Integer)
assert_call('module IRB; VERSION.', include: String)
assert_call('module IRB; IRB::VERSION.', include: String)
assert_call('module IRB; VERSION=1; VERSION.', include: Integer)
assert_call('module IRB; VERSION=1; IRB::VERSION.', include: Integer)
assert_call('module IRB; module A; VERSION.', include: String)
assert_call('module IRB; module A; VERSION=1; VERSION.', include: Integer)
assert_call('module IRB; module A; VERSION=1; IRB::VERSION.', include: String)
assert_call('module IRB; module A; VERSION=1; end; VERSION.', include: String)
assert_call('module IRB; IRB=1; IRB.', include: Integer)
assert_call('module IRB; IRB=1; ::IRB::VERSION.', include: String)
module_binding = eval 'module ::IRB; binding; end'
assert_call('VERSION.', include: NilClass)
assert_call('VERSION.', include: String, binding: module_binding)
assert_call('IRB::VERSION.', include: String, binding: module_binding)
assert_call('A = 1; module M; A += 0.5; A.', include: Float)
assert_call('::A = 1; module M; A += 0.5; A.', include: Float)
assert_call('::A = 1; module M; A += 0.5; ::A.', include: Integer)
assert_call('IRB::A = 1; IRB::A += 0.5; IRB::A.', include: Float)
end
def test_literal
assert_call('1.', include: Integer)
assert_call('1.0.', include: Float)
assert_call('1r.', include: Rational)
assert_call('1i.', include: Complex)
assert_call('true.', include: TrueClass)
assert_call('false.', include: FalseClass)
assert_call('nil.', include: NilClass)
assert_call('().', include: NilClass)
assert_call('//.', include: Regexp)
assert_call('/#{a=1}/.', include: Regexp)
assert_call('/#{a=1}/; a.', include: Integer)
assert_call(':a.', include: Symbol)
assert_call(':"#{a=1}".', include: Symbol)
assert_call(':"#{a=1}"; a.', include: Integer)
assert_call('"".', include: String)
assert_call('"#$a".', include: String)
assert_call('("a" "b").', include: String)
assert_call('"#{a=1}".', include: String)
assert_call('"#{a=1}"; a.', include: Integer)
assert_call('``.', include: String)
assert_call('`#{a=1}`.', include: String)
assert_call('`#{a=1}`; a.', include: Integer)
end
def test_redo_retry_yield_super
assert_call('a=nil; tap do a=1; redo; a=1i; end; a.', include: Integer, exclude: Complex)
assert_call('a=nil; tap do a=1; retry; a=1i; end; a.', include: Integer, exclude: Complex)
assert_call('a = 0; a = yield; a.', include: Object, exclude: Integer)
assert_call('yield 1,(a=1); a.', include: Integer)
assert_call('a = 0; a = super; a.', include: Object, exclude: Integer)
assert_call('a = 0; a = super(1); a.', include: Object, exclude: Integer)
assert_call('super 1,(a=1); a.', include: Integer)
end
def test_rarely_used_syntax
# FlipFlop
assert_call('if (a=1).even?..(a=1.0).even; a.', include: [Integer, Float])
# MatchLastLine
assert_call('if /regexp/; 1.', include: Integer)
assert_call('if /reg#{a=1}exp/; a.', include: Integer)
# BlockLocalVariable
assert_call('tap do |i;a| a=1; a.', include: Integer)
# BEGIN{} END{}
assert_call('BEGIN{1.', include: Integer)
assert_call('END{1.', include: Integer)
# MatchWrite
assert_call('a=1; /(?<a>)/=~b; a.', include: [String, NilClass], exclude: Integer)
# OperatorWrite with block `a[&b]+=c`
assert_call('a=[1]; (a[0,&:to_a]+=1.0).', include: Float)
assert_call('a=[1]; (a[0,&b]+=1.0).', include: Float)
end
def test_hash
assert_call('{}.', include: Hash)
assert_call('{**a}.', include: Hash)
assert_call('{ rand: }.values.sample.', include: Float)
assert_call('rand=""; { rand: }.values.sample.', include: String, exclude: Float)
assert_call('{ 1 => 1.0 }.keys.sample.', include: Integer, exclude: Float)
assert_call('{ 1 => 1.0 }.values.sample.', include: Float, exclude: Integer)
assert_call('a={1=>1.0}; {"a"=>1i,**a}.keys.sample.', include: [Integer, String])
assert_call('a={1=>1.0}; {"a"=>1i,**a}.values.sample.', include: [Float, Complex])
end
def test_array
assert_call('[1,2,3].sample.', include: Integer)
assert_call('a = 1.0; [1,2,a].sample.', include: [Integer, Float])
assert_call('a = [1.0]; [1,2,*a].sample.', include: [Integer, Float])
end
def test_numbered_parameter
assert_call('loop{_1.', include: NilClass)
assert_call('1.tap{_1.', include: Integer)
assert_call('1.tap{_3.', include: NilClass, exclude: Integer)
assert_call('[:a,1].tap{_1.', include: Array, exclude: [Integer, Symbol])
assert_call('[:a,1].tap{_2.', include: [Symbol, Integer], exclude: Array)
assert_call('[:a,1].tap{_2; _1.', include: [Symbol, Integer], exclude: Array)
assert_call('[:a].each_with_index{_1.', include: Symbol, exclude: [Integer, Array])
assert_call('[:a].each_with_index{_2; _1.', include: Symbol, exclude: [Integer, Array])
assert_call('[:a].each_with_index{_2.', include: Integer, exclude: Symbol)
end
def test_if_unless
assert_call('if cond; 1; end.', include: Integer)
assert_call('unless true; 1; end.', include: Integer)
assert_call('a=1; (a=1.0) if cond; a.', include: [Integer, Float])
assert_call('a=1; (a=1.0) unless cond; a.', include: [Integer, Float])
assert_call('a=1; 123 if (a=1.0).foo; a.', include: Float, exclude: Integer)
assert_call('if cond; a=1; end; a.', include: [Integer, NilClass])
assert_call('a=1; if cond; a=1.0; elsif cond; a=1r; else; a=1i; end; a.', include: [Float, Rational, Complex], exclude: Integer)
assert_call('a=1; if cond; a=1.0; else; a.', include: Integer, exclude: Float)
assert_call('a=1; if (a=1.0).foo; a.', include: Float, exclude: Integer)
assert_call('a=1; if (a=1.0).foo; end; a.', include: Float, exclude: Integer)
assert_call('a=1; if (a=1.0).foo; else; a.', include: Float, exclude: Integer)
assert_call('a=1; if (a=1.0).foo; elsif a.', include: Float, exclude: Integer)
assert_call('a=1; if (a=1.0).foo; elsif (a=1i); else; a.', include: Complex, exclude: [Integer, Float])
end
def test_while_until
assert_call('while cond; 123; end.', include: NilClass)
assert_call('until cond; 123; end.', include: NilClass)
assert_call('a=1; a=1.0 while cond; a.', include: [Integer, Float])
assert_call('a=1; a=1.0 until cond; a.', include: [Integer, Float])
assert_call('a=1; 1 while (a=1.0).foo; a.', include: Float, exclude: Integer)
assert_call('while cond; break 1; end.', include: Integer)
assert_call('while cond; a=1; end; a.', include: Integer)
assert_call('a=1; while cond; a=1.0; end; a.', include: [Integer, Float])
assert_call('a=1; while (a=1.0).foo; end; a.', include: Float, exclude: Integer)
end
def test_for
assert_call('for i in [1,2,3]; i.', include: Integer)
assert_call('for i,j in [1,2,3]; i.', include: Integer)
assert_call('for *,(*) in [1,2,3]; 1.', include: Integer)
assert_call('for *i in [1,2,3]; i.sample.', include: Integer)
assert_call('for (a=1).b in [1,2,3]; a.', include: Integer)
assert_call('for Array::B in [1,2,3]; Array::B.', include: Integer)
assert_call('for A in [1,2,3]; A.', include: Integer)
assert_call('for $a in [1,2,3]; $a.', include: Integer)
assert_call('for @a in [1,2,3]; @a.', include: Integer)
assert_call('for i in [1,2,3]; end.', include: Array)
assert_call('for i in [1,2,3]; break 1.0; end.', include: [Array, Float])
assert_call('i = 1.0; for i in [1,2,3]; end; i.', include: [Integer, Float])
assert_call('a = 1.0; for i in [1,2,3]; a = 1i; end; a.', include: [Float, Complex])
end
def test_special_var
assert_call('__FILE__.', include: String)
assert_call('__LINE__.', include: Integer)
assert_call('__ENCODING__.', include: Encoding)
assert_call('$1.', include: String)
assert_call('$&.', include: String)
end
def test_and_or
assert_call('(1&&1.0).', include: Float, exclude: Integer)
assert_call('(nil&&1.0).', include: NilClass)
assert_call('(nil||1).', include: Integer)
assert_call('(1||1.0).', include: Float)
end
def test_opwrite
assert_call('a=[]; a*=1; a.', include: Array)
assert_call('a=[]; a*=""; a.', include: String)
assert_call('a=[1,false].sample; a||=1.0; a.', include: [Integer, Float])
assert_call('a=1; a&&=1.0; a.', include: Float, exclude: Integer)
assert_call('(a=1).b*=1; a.', include: Integer)
assert_call('(a=1).b||=1; a.', include: Integer)
assert_call('(a=1).b&&=1; a.', include: Integer)
assert_call('[][a=1]&&=1; a.', include: Integer)
assert_call('[][a=1]||=1; a.', include: Integer)
assert_call('[][a=1]+=1; a.', include: Integer)
assert_call('([1][0]+=1.0).', include: Float)
assert_call('([1.0][0]+=1).', include: Float)
assert_call('A=nil; A||=1; A.', include: Integer)
assert_call('A=1; A&&=1.0; A.', include: Float)
assert_call('A=1; A+=1.0; A.', include: Float)
assert_call('Array::A||=1; Array::A.', include: Integer)
assert_call('Array::A=1; Array::A&&=1.0; Array::A.', include: Float)
end
def test_case_when
assert_call('case x; when A; 1; when B; 1.0; end.', include: [Integer, Float, NilClass])
assert_call('case x; when A; 1; when B; 1.0; else; 1r; end.', include: [Integer, Float, Rational], exclude: NilClass)
assert_call('case; when (a=1); a.', include: Integer)
assert_call('case x; when (a=1); a.', include: Integer)
assert_call('a=1; case (a=1.0); when A; a.', include: Float, exclude: Integer)
assert_call('a=1; case (a=1.0); when A; end; a.', include: Float, exclude: Integer)
assert_call('a=1; case x; when A; a=1.0; else; a=1r; end; a.', include: [Float, Rational], exclude: Integer)
assert_call('a=1; case x; when A; a=1.0; when B; a=1r; end; a.', include: [Float, Rational, Integer])
end
def test_case_in
assert_call('case x; in A; 1; in B; 1.0; end.', include: [Integer, Float], exclude: NilClass)
assert_call('case x; in A; 1; in B; 1.0; else; 1r; end.', include: [Integer, Float, Rational], exclude: NilClass)
assert_call('a=""; case 1; in A; a=1; in B; a=1.0; end; a.', include: [Integer, Float], exclude: String)
assert_call('a=""; case 1; in A; a=1; in B; a=1.0; else; a=1r; end; a.', include: [Integer, Float, Rational], exclude: String)
assert_call('case 1; in x; x.', include: Integer)
assert_call('case x; in A if (a=1); a.', include: Integer)
assert_call('case x; in ^(a=1); a.', include: Integer)
assert_call('case x; in [1, String => a, 2]; a.', include: String)
assert_call('case x; in [*a, 1]; a.', include: Array)
assert_call('case x; in [1, *a]; a.', include: Array)
assert_call('case x; in [*a, 1, *b]; a.', include: Array)
assert_call('case x; in [*a, 1, *b]; b.', include: Array)
assert_call('case x; in {a: {b: **c}}; c.', include: Hash)
assert_call('case x; in (String | { x: Integer, y: ^$a }) => a; a.', include: [String, Hash])
end
def test_pattern_match
assert_call('1 in a; a.', include: Integer)
assert_call('a=1; x in String=>a; a.', include: [Integer, String])
assert_call('a=1; x=>String=>a; a.', include: String, exclude: Integer)
end
def test_bottom_type_termination
assert_call('a=1; tap { raise; a=1.0; a.', include: Float)
assert_call('a=1; tap { loop{}; a=1.0; a.', include: Float)
assert_call('a=1; tap { raise; a=1.0 } a.', include: Integer, exclude: Float)
assert_call('a=1; tap { loop{}; a=1.0 } a.', include: Integer, exclude: Float)
end
def test_call_parameter
assert_call('f((x=1),*b,c:1,**d,&e); x.', include: Integer)
assert_call('f(a,*(x=1),c:1,**d,&e); x.', include: Integer)
assert_call('f(a,*b,(x=1):1,**d,&e); x.', include: Integer)
assert_call('f(a,*b,c:(x=1),**d,&e); x.', include: Integer)
assert_call('f(a,*b,c:1,**(x=1),&e); x.', include: Integer)
assert_call('f(a,*b,c:1,**d,&(x=1)); x.', include: Integer)
assert_call('f((x=1)=>1); x.', include: Integer)
end
def test_block_args
assert_call('[1,2,3].tap{|a| a.', include: Array)
assert_call('[1,2,3].tap{|a,b| a.', include: Integer)
assert_call('[1,2,3].tap{|(a,b)| a.', include: Integer)
assert_call('[1,2,3].tap{|a,*b| b.', include: Array)
assert_call('[1,2,3].tap{|a=1.0| a.', include: [Array, Float])
assert_call('[1,2,3].tap{|a,**b| b.', include: Hash)
assert_call('1.tap{|(*),*,**| 1.', include: Integer)
end
def test_array_aref
assert_call('[1][0..].', include: [Array, NilClass], exclude: Integer)
assert_call('[1][0].', include: Integer, exclude: [Array, NilClass])
assert_call('[1].[](0).', include: Integer, exclude: [Array, NilClass])
assert_call('[1].[](0){}.', include: Integer, exclude: [Array, NilClass])
end
end
end

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

@ -0,0 +1,181 @@
# frozen_string_literal: true
# Run test only when Ruby >= 3.0 and %w[prism rbs] are available
return unless RUBY_VERSION >= '3.0.0'
return if RUBY_ENGINE == 'truffleruby' # needs endless method definition
begin
require 'prism'
require 'rbs'
rescue LoadError
return
end
require 'irb/type_completion/completor'
require_relative '../helper'
module TestIRB
class TypeCompletorTest < TestCase
def setup
IRB::TypeCompletion::Types.load_rbs_builder unless IRB::TypeCompletion::Types.rbs_builder
@completor = IRB::TypeCompletion::Completor.new
end
def empty_binding
binding
end
TARGET_REGEXP = /(@@|@|\$)?[a-zA-Z_]*[!?=]?$/
def assert_completion(code, binding: empty_binding, include: nil, exclude: nil)
raise ArgumentError if include.nil? && exclude.nil?
target = code[TARGET_REGEXP]
candidates = @completor.completion_candidates(code.delete_suffix(target), target, '', bind: binding)
assert ([*include] - candidates).empty?, "Expected #{candidates} to include #{include}" if include
assert (candidates & [*exclude]).empty?, "Expected #{candidates} not to include #{exclude}" if exclude
end
def assert_doc_namespace(code, namespace, binding: empty_binding)
target = code[TARGET_REGEXP]
preposing = code.delete_suffix(target)
@completor.completion_candidates(preposing, target, '', bind: binding)
assert_equal namespace, @completor.doc_namespace(preposing, target, '', bind: binding)
end
def test_require
assert_completion("require '", include: 'set')
assert_completion("require 's", include: 'set')
Dir.chdir(__dir__ + "/../../..") do
assert_completion("require_relative 'l", include: 'lib/irb')
end
# Incomplete double quote string is InterpolatedStringNode
assert_completion('require "', include: 'set')
assert_completion('require "s', include: 'set')
end
def test_method_block_sym
assert_completion('[1].map(&:', include: 'abs')
assert_completion('[:a].map(&:', exclude: 'abs')
assert_completion('[1].map(&:a', include: 'abs')
assert_doc_namespace('[1].map(&:abs', 'Integer#abs')
end
def test_symbol
sym = :test_completion_symbol
assert_completion(":test_com", include: sym.to_s)
end
def test_call
assert_completion('1.', include: 'abs')
assert_completion('1.a', include: 'abs')
assert_completion('ran', include: 'rand')
assert_doc_namespace('1.abs', 'Integer#abs')
assert_doc_namespace('Integer.sqrt', 'Integer.sqrt')
assert_doc_namespace('rand', 'TestIRB::TypeCompletorTest#rand')
assert_doc_namespace('Object::rand', 'Object.rand')
end
def test_lvar
bind = eval('lvar = 1; binding')
assert_completion('lva', binding: bind, include: 'lvar')
assert_completion('lvar.', binding: bind, include: 'abs')
assert_completion('lvar.a', binding: bind, include: 'abs')
assert_completion('lvar = ""; lvar.', binding: bind, include: 'ascii_only?')
assert_completion('lvar = ""; lvar.', include: 'ascii_only?')
assert_doc_namespace('lvar', 'Integer', binding: bind)
assert_doc_namespace('lvar.abs', 'Integer#abs', binding: bind)
assert_doc_namespace('lvar = ""; lvar.ascii_only?', 'String#ascii_only?', binding: bind)
end
def test_const
assert_completion('Ar', include: 'Array')
assert_completion('::Ar', include: 'Array')
assert_completion('IRB::V', include: 'VERSION')
assert_completion('FooBar=1; F', include: 'FooBar')
assert_completion('::FooBar=1; ::F', include: 'FooBar')
assert_doc_namespace('Array', 'Array')
assert_doc_namespace('Array = 1; Array', 'Integer')
assert_doc_namespace('Object::Array', 'Array')
assert_completion('::', include: 'Array')
assert_completion('class ::', include: 'Array')
assert_completion('module IRB; class T', include: ['TypeCompletion', 'TracePoint'])
end
def test_gvar
assert_completion('$', include: '$stdout')
assert_completion('$s', include: '$stdout')
assert_completion('$', exclude: '$foobar')
assert_completion('$foobar=1; $', include: '$foobar')
assert_doc_namespace('$foobar=1; $foobar', 'Integer')
assert_doc_namespace('$stdout', 'IO')
assert_doc_namespace('$stdout=1; $stdout', 'Integer')
end
def test_ivar
bind = Object.new.instance_eval { @foo = 1; binding }
assert_completion('@', binding: bind, include: '@foo')
assert_completion('@f', binding: bind, include: '@foo')
assert_completion('@bar = 1; @', include: '@bar')
assert_completion('@bar = 1; @b', include: '@bar')
assert_doc_namespace('@bar = 1; @bar', 'Integer')
assert_doc_namespace('@foo', 'Integer', binding: bind)
assert_doc_namespace('@foo = 1.0; @foo', 'Float', binding: bind)
end
def test_cvar
bind = eval('m=Module.new; module m::M; @@foo = 1; binding; end')
assert_equal(1, bind.eval('@@foo'))
assert_completion('@', binding: bind, include: '@@foo')
assert_completion('@@', binding: bind, include: '@@foo')
assert_completion('@@f', binding: bind, include: '@@foo')
assert_doc_namespace('@@foo', 'Integer', binding: bind)
assert_doc_namespace('@@foo = 1.0; @@foo', 'Float', binding: bind)
assert_completion('@@bar = 1; @', include: '@@bar')
assert_completion('@@bar = 1; @@', include: '@@bar')
assert_completion('@@bar = 1; @@b', include: '@@bar')
assert_doc_namespace('@@bar = 1; @@bar', 'Integer')
end
def test_basic_object
bo = BasicObject.new
def bo.foo; end
bo.instance_eval { @bar = 1 }
bind = binding
bo_self_bind = bo.instance_eval { Kernel.binding }
assert_completion('bo.', binding: bind, include: 'foo')
assert_completion('def bo.baz; self.', binding: bind, include: 'foo')
assert_completion('[bo].first.', binding: bind, include: 'foo')
assert_doc_namespace('bo', 'BasicObject', binding: bind)
assert_doc_namespace('bo.__id__', 'BasicObject#__id__', binding: bind)
assert_doc_namespace('v = [bo]; v', 'Array', binding: bind)
assert_doc_namespace('v = [bo].first; v', 'BasicObject', binding: bind)
bo_self_bind = bo.instance_eval { Kernel.binding }
assert_completion('self.', binding: bo_self_bind, include: 'foo')
assert_completion('@', binding: bo_self_bind, include: '@bar')
assert_completion('@bar.', binding: bo_self_bind, include: 'abs')
assert_doc_namespace('self.__id__', 'BasicObject#__id__', binding: bo_self_bind)
assert_doc_namespace('@bar', 'Integer', binding: bo_self_bind)
if RUBY_VERSION >= '3.2.0' # Needs Class#attached_object to get instance variables from singleton class
assert_completion('def bo.baz; @bar.', binding: bind, include: 'abs')
assert_completion('def bo.baz; @', binding: bind, include: '@bar')
end
end
def test_inspect
rbs_builder = IRB::TypeCompletion::Types.rbs_builder
assert_match(/TypeCompletion::Completor\(Prism: \d.+, RBS: \d.+\)/, @completor.inspect)
IRB::TypeCompletion::Types.instance_variable_set(:@rbs_builder, nil)
assert_match(/TypeCompletion::Completor\(Prism: \d.+, RBS: loading\)/, @completor.inspect)
IRB::TypeCompletion::Types.instance_variable_set(:@rbs_load_error, StandardError.new('[err]'))
assert_match(/TypeCompletion::Completor\(Prism: \d.+, RBS: .+\[err\].+\)/, @completor.inspect)
ensure
IRB::TypeCompletion::Types.instance_variable_set(:@rbs_builder, rbs_builder)
IRB::TypeCompletion::Types.instance_variable_set(:@rbs_load_error, nil)
end
def test_none
candidates = @completor.completion_candidates('(', ')', '', bind: binding)
assert_equal [], candidates
assert_doc_namespace('()', nil)
end
end
end

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

@ -0,0 +1,89 @@
# frozen_string_literal: true
return unless RUBY_VERSION >= '3.0.0'
return if RUBY_ENGINE == 'truffleruby' # needs endless method definition
require 'irb/type_completion/types'
require_relative '../helper'
module TestIRB
class TypeCompletionTypesTest < TestCase
def test_type_inspect
true_type = IRB::TypeCompletion::Types::TRUE
false_type = IRB::TypeCompletion::Types::FALSE
nil_type = IRB::TypeCompletion::Types::NIL
string_type = IRB::TypeCompletion::Types::STRING
true_or_false = IRB::TypeCompletion::Types::UnionType[true_type, false_type]
array_type = IRB::TypeCompletion::Types::InstanceType.new Array, { Elem: true_or_false }
assert_equal 'nil', nil_type.inspect
assert_equal 'true', true_type.inspect
assert_equal 'false', false_type.inspect
assert_equal 'String', string_type.inspect
assert_equal 'Array', IRB::TypeCompletion::Types::InstanceType.new(Array).inspect
assert_equal 'true | false', true_or_false.inspect
assert_equal 'Array[Elem: true | false]', array_type.inspect
assert_equal 'Array', array_type.inspect_without_params
assert_equal 'Proc', IRB::TypeCompletion::Types::PROC.inspect
assert_equal 'Array.itself', IRB::TypeCompletion::Types::SingletonType.new(Array).inspect
end
def test_type_from_object
obj = Object.new
bo = BasicObject.new
def bo.hash; 42; end # Needed to use this object as a hash key
arr = [1, 'a']
hash = { 'key' => :value }
int_type = IRB::TypeCompletion::Types.type_from_object 1
obj_type = IRB::TypeCompletion::Types.type_from_object obj
arr_type = IRB::TypeCompletion::Types.type_from_object arr
hash_type = IRB::TypeCompletion::Types.type_from_object hash
bo_type = IRB::TypeCompletion::Types.type_from_object bo
bo_arr_type = IRB::TypeCompletion::Types.type_from_object [bo]
bo_key_hash_type = IRB::TypeCompletion::Types.type_from_object({ bo => 1 })
bo_value_hash_type = IRB::TypeCompletion::Types.type_from_object({ x: bo })
assert_equal Integer, int_type.klass
# Use singleton_class to autocomplete singleton methods
assert_equal obj.singleton_class, obj_type.klass
assert_equal Object.instance_method(:singleton_class).bind_call(bo), bo_type.klass
# Array and Hash are special
assert_equal Array, arr_type.klass
assert_equal Array, bo_arr_type.klass
assert_equal Hash, hash_type.klass
assert_equal Hash, bo_key_hash_type.klass
assert_equal Hash, bo_value_hash_type.klass
assert_equal BasicObject, bo_arr_type.params[:Elem].klass
assert_equal BasicObject, bo_key_hash_type.params[:K].klass
assert_equal BasicObject, bo_value_hash_type.params[:V].klass
assert_equal 'Object', obj_type.inspect
assert_equal 'Array[Elem: Integer | String]', arr_type.inspect
assert_equal 'Hash[K: String, V: Symbol]', hash_type.inspect
assert_equal 'Array.itself', IRB::TypeCompletion::Types.type_from_object(Array).inspect
assert_equal 'IRB::TypeCompletion.itself', IRB::TypeCompletion::Types.type_from_object(IRB::TypeCompletion).inspect
end
def test_type_methods
s = +''
class << s
def foobar; end
private def foobaz; end
end
String.define_method(:foobarbaz) {}
targets = [:foobar, :foobaz, :foobarbaz]
type = IRB::TypeCompletion::Types.type_from_object s
assert_equal [:foobar, :foobarbaz], targets & type.methods
assert_equal [:foobar, :foobaz, :foobarbaz], targets & type.all_methods
assert_equal [:foobarbaz], targets & IRB::TypeCompletion::Types::STRING.methods
assert_equal [:foobarbaz], targets & IRB::TypeCompletion::Types::STRING.all_methods
ensure
String.remove_method :foobarbaz
end
def test_basic_object_methods
bo = BasicObject.new
def bo.foobar; end
type = IRB::TypeCompletion::Types.type_from_object bo
assert type.all_methods.include?(:foobar)
end
end
end