[ruby/irb] Use gem repl_type_completor, remove type_completion

implementation
(https://github.com/ruby/irb/pull/772)

https://github.com/ruby/irb/commit/a4868a5373
This commit is contained in:
tomoya ishida 2023-11-30 01:30:08 +09:00 коммит произвёл git
Родитель b549722eef
Коммит 86d9a6dcb6
13 изменённых файлов: 114 добавлений и 3372 удалений

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

@ -93,6 +93,27 @@ module IRB
end
end
class TypeCompletor < BaseCompletor # :nodoc:
def initialize(context)
@context = context
end
def inspect
ReplTypeCompletor.info
end
def completion_candidates(preposing, target, _postposing, bind:)
result = ReplTypeCompletor.analyze(preposing + target, binding: bind, filename: @context.irb_path)
return [] unless result
result.completion_candidates.map { target + _1 }
end
def doc_namespace(preposing, matched, _postposing, bind:)
result = ReplTypeCompletor.analyze(preposing + matched, binding: bind, filename: @context.irb_path)
result&.doc_namespace('')
end
end
class RegexpCompletor < BaseCompletor # :nodoc:
using Module.new {
refine ::Binding do

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

@ -176,26 +176,22 @@ module IRB
RegexpCompletor.new
end
TYPE_COMPLETION_REQUIRED_PRISM_VERSION = '0.18.0'
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'
if RUBY_ENGINE == 'truffleruby'
# Avoid SynatxError. truffleruby does not support endless method definition yet.
warn 'TypeCompletor is not supported on TruffleRuby yet'
return
end
begin
require 'prism'
require 'repl_type_completor'
rescue LoadError => e
warn "TypeCompletion requires Prism: #{e.message}"
warn "TypeCompletor requires `gem repl_type_completor`: #{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
ReplTypeCompletor.preload_rbs
TypeCompletor.new(self)
end
def save_history=(val)

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

@ -1,241 +0,0 @@
# 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:)
verbose, $VERBOSE = $VERBOSE, nil
@preposing = preposing
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 Exception => e
handle_error(e)
[]
ensure
$VERBOSE = verbose
end
def doc_namespace(preposing, matched, postposing, bind:)
verbose, $VERBOSE = $VERBOSE, nil
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
rescue Exception => e
handle_error(e)
nil
ensure
$VERBOSE = verbose
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)
Completor.last_completion_error = e
end
end
end
end

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

@ -1,13 +0,0 @@
# 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

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

@ -1,412 +0,0 @@
# 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

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

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

@ -1,426 +0,0 @@
# 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

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

@ -666,14 +666,9 @@ module TestIRB
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
# :type is tested in test_type_completor.rb
ensure
$VERBOSE = verbose
IRB.conf[:COMPLETOR] = original_completor

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

@ -0,0 +1,83 @@
# frozen_string_literal: true
# Run test only when Ruby >= 3.0 and repl_type_completor is available
return unless RUBY_VERSION >= '3.0.0'
return if RUBY_ENGINE == 'truffleruby' # needs endless method definition
begin
require 'repl_type_completor'
rescue LoadError
return
end
require 'irb/completion'
require 'tempfile'
require_relative './helper'
module TestIRB
class TypeCompletorTest < TestCase
DummyContext = Struct.new(:irb_path)
def setup
ReplTypeCompletor.load_rbs unless ReplTypeCompletor.rbs_loaded?
context = DummyContext.new('(irb)')
@completor = IRB::TypeCompletor.new(context)
end
def empty_binding
binding
end
def assert_completion(preposing, target, binding: empty_binding, include: nil, exclude: nil)
raise ArgumentError if include.nil? && exclude.nil?
candidates = @completor.completion_candidates(preposing, 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(preposing, target, namespace, binding: empty_binding)
@completor.completion_candidates(preposing, target, '', bind: binding)
assert_equal namespace, @completor.doc_namespace(preposing, target, '', bind: binding)
end
def test_type_completion
bind = eval('num = 1; binding')
assert_completion('num.times.map(&:', 'ab', binding: bind, include: 'abs')
assert_doc_namespace('num.chr.', 'upcase', 'String#upcase', binding: bind)
end
def test_inspect
assert_match(/\AReplTypeCompletor.*\z/, @completor.inspect)
end
def test_empty_completion
candidates = @completor.completion_candidates('(', ')', '', bind: binding)
assert_equal [], candidates
assert_doc_namespace('(', ')', nil)
end
end
class TypeCompletorIntegrationTest < IntegrationTestCase
def test_type_completor
write_rc <<~RUBY
IRB.conf[:COMPLETOR] = :type
RUBY
write_ruby <<~'RUBY'
binding.irb
RUBY
output = run_ruby_file do
type "irb_info"
type "sleep 0.01 until ReplTypeCompletor.rbs_loaded?"
type "completor = IRB.CurrentContext.io.instance_variable_get(:@completor);"
type "n = 10"
type "puts completor.completion_candidates 'a = n.abs;', 'a.b', '', bind: binding"
type "puts completor.doc_namespace 'a = n.chr;', 'a.encoding', '', bind: binding"
type "exit!"
end
assert_match(/Completion: Autocomplete, ReplTypeCompletor/, output)
assert_match(/a\.bit_length/, output)
assert_match(/String#encoding/, output)
end
end
end

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

@ -1,112 +0,0 @@
# 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

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

@ -1,697 +0,0 @@
# 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/version'
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

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

@ -1,182 +0,0 @@
# 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/version'
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

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

@ -1,89 +0,0 @@
# 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