зеркало из https://github.com/github/ruby.git
[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:
Родитель
b549722eef
Коммит
86d9a6dcb6
|
@ -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
|
Загрузка…
Ссылка в новой задаче