From 6aaf8073bb13dbce875961aa8d518a6c34478d29 Mon Sep 17 00:00:00 2001 From: tompng Date: Sat, 18 Nov 2023 05:43:59 +0900 Subject: [PATCH] Use gem repl_type_completor, remove type_completion implementation --- Gemfile | 5 +- README.md | 18 +- Rakefile | 2 +- lib/irb/completion.rb | 21 + lib/irb/context.rb | 22 +- lib/irb/type_completion/completor.rb | 241 ---- lib/irb/type_completion/methods.rb | 13 - lib/irb/type_completion/scope.rb | 412 ------ lib/irb/type_completion/type_analyzer.rb | 1181 ----------------- lib/irb/type_completion/types.rb | 426 ------ test/irb/test_context.rb | 7 +- test/irb/test_type_completor.rb | 83 ++ test/irb/type_completion/test_scope.rb | 112 -- test/irb/type_completion/test_type_analyze.rb | 697 ---------- .../type_completion/test_type_completor.rb | 182 --- test/irb/type_completion/test_types.rb | 89 -- 16 files changed, 126 insertions(+), 3385 deletions(-) delete mode 100644 lib/irb/type_completion/completor.rb delete mode 100644 lib/irb/type_completion/methods.rb delete mode 100644 lib/irb/type_completion/scope.rb delete mode 100644 lib/irb/type_completion/type_analyzer.rb delete mode 100644 lib/irb/type_completion/types.rb create mode 100644 test/irb/test_type_completor.rb delete mode 100644 test/irb/type_completion/test_scope.rb delete mode 100644 test/irb/type_completion/test_type_analyze.rb delete mode 100644 test/irb/type_completion/test_type_completor.rb delete mode 100644 test/irb/type_completion/test_types.rb diff --git a/Gemfile b/Gemfile index 11be29977..940387ea7 100644 --- a/Gemfile +++ b/Gemfile @@ -18,7 +18,6 @@ gem "test-unit" gem "test-unit-ruby-core" gem "debug", github: "ruby/debug" -if RUBY_VERSION >= "3.0.0" - gem "rbs" - gem "prism", ">= 0.18.0" +if RUBY_VERSION >= "3.0.0" && !is_truffleruby + gem "repl_type_completor" end diff --git a/README.md b/README.md index 483fb9a63..9425f4341 100644 --- a/README.md +++ b/README.md @@ -237,11 +237,11 @@ However, there are also some limitations to be aware of: ## Type Based Completion -IRB's default completion `IRB::RegexpCompletor` uses Regexp. IRB has another experimental completion `IRB::TypeCompletion` that uses type analysis. +IRB's default completion `IRB::RegexpCompletor` uses Regexp. IRB has another experimental completion `IRB::TypeCompletor` that uses type analysis. -### How to Enable IRB::TypeCompletion +### How to Enable IRB::TypeCompletor -To enable IRB::TypeCompletion, run IRB with `--type-completor` option +To enable IRB::TypeCompletor, run IRB with `--type-completor` option ``` $ irb --type-completor ``` @@ -249,14 +249,14 @@ Or write the code below to IRB's rc-file. ```ruby IRB.conf[:COMPLETOR] = :type # default is :regexp ``` -You also need `gem prism` and `gem rbs` to use this feature. +You also need `gem repl_type_completor` to use this feature. To check if it's enabled, type `irb_info` into IRB and see the `Completion` section. ``` irb(main):001> irb_info ... # Enabled -Completion: Autocomplete, TypeCompletion::Completor(Prism: 0.17.1, RBS: 3.3.0) +Completion: Autocomplete, ReplTypeCompletor: 0.1.0, Prism: 0.18.0, RBS: 3.3.0 # Not enabled Completion: Autocomplete, RegexpCompletor ... @@ -265,7 +265,7 @@ If you have `sig/` directory or `rbs_collection.lock.yaml` in current directory, ### Advantage over Default IRB::RegexpCompletor -IRB::TypeCompletion can autocomplete chained methods, block parameters and more if type information is available. +IRB::TypeCompletor can autocomplete chained methods, block parameters and more if type information is available. These are some examples IRB::RegexpCompletor cannot complete. ```ruby @@ -287,11 +287,11 @@ As a trade-off, completion calculation takes more time than IRB::RegexpCompletor ### Difference between Steep's Completion -Compared with Steep, IRB::TypeCompletion has some difference and limitations. +Compared with Steep, IRB::TypeCompletor has some difference and limitations. ```ruby [0, 'a'].sample. # Steep completes intersection of Integer methods and String methods -# IRB::TypeCompletion completes both Integer and String methods +# IRB::TypeCompletor completes both Integer and String methods ``` Some features like type narrowing is not implemented. @@ -301,7 +301,7 @@ def f(arg = [0, 'a'].sample) arg. # Completes both Integer and String methods ``` -Unlike other static type checker, IRB::TypeCompletion uses runtime information to provide better completion. +Unlike other static type checker, IRB::TypeCompletor uses runtime information to provide better completion. ```ruby irb(main):001> a = [1] => [1] diff --git a/Rakefile b/Rakefile index 9231f6985..01bd83cb2 100644 --- a/Rakefile +++ b/Rakefile @@ -13,7 +13,7 @@ desc "Run each irb test file in isolation." task :test_in_isolation do failed = false - FileList["test/irb/test_*.rb", "test/irb/type_completion/test_*.rb"].each do |test_file| + FileList["test/irb/**/test_*.rb"].each do |test_file| ENV["TEST"] = test_file begin Rake::Task["test"].execute diff --git a/lib/irb/completion.rb b/lib/irb/completion.rb index 9b29a787b..af3b69eb2 100644 --- a/lib/irb/completion.rb +++ b/lib/irb/completion.rb @@ -93,6 +93,27 @@ def retrieve_files_to_require_relative_from_current_dir 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 diff --git a/lib/irb/context.rb b/lib/irb/context.rb index 3442fbf4d..ffbba4e8b 100644 --- a/lib/irb/context.rb +++ b/lib/irb/context.rb @@ -176,26 +176,22 @@ def initialize(irb, workspace = nil, input_method = nil) 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) diff --git a/lib/irb/type_completion/completor.rb b/lib/irb/type_completion/completor.rb deleted file mode 100644 index df1e1c779..000000000 --- a/lib/irb/type_completion/completor.rb +++ /dev/null @@ -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 diff --git a/lib/irb/type_completion/methods.rb b/lib/irb/type_completion/methods.rb deleted file mode 100644 index 8a88b6d0f..000000000 --- a/lib/irb/type_completion/methods.rb +++ /dev/null @@ -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 diff --git a/lib/irb/type_completion/scope.rb b/lib/irb/type_completion/scope.rb deleted file mode 100644 index 5a58a0ed6..000000000 --- a/lib/irb/type_completion/scope.rb +++ /dev/null @@ -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 diff --git a/lib/irb/type_completion/type_analyzer.rb b/lib/irb/type_completion/type_analyzer.rb deleted file mode 100644 index 344924c9f..000000000 --- a/lib/irb/type_completion/type_analyzer.rb +++ /dev/null @@ -1,1181 +0,0 @@ -# frozen_string_literal: true - -require 'set' -require_relative 'types' -require_relative 'scope' -require 'prism' - -module IRB - module TypeCompletion - class TypeAnalyzer - class DigTarget - def initialize(parents, receiver, &block) - @dig_ids = parents.to_h { [_1.__id__, true] } - @target_id = receiver.__id__ - @block = block - end - - def dig?(node) = @dig_ids[node.__id__] - def target?(node) = @target_id == node.__id__ - def resolve(type, scope) - @block.call type, scope - end - end - - OBJECT_METHODS = { - to_s: Types::STRING, - to_str: Types::STRING, - to_a: Types::ARRAY, - to_ary: Types::ARRAY, - to_h: Types::HASH, - to_hash: Types::HASH, - to_i: Types::INTEGER, - to_int: Types::INTEGER, - to_f: Types::FLOAT, - to_c: Types::COMPLEX, - to_r: Types::RATIONAL - } - - def initialize(dig_targets) - @dig_targets = dig_targets - end - - def evaluate(node, scope) - method = "evaluate_#{node.type}" - if respond_to? method - result = send method, node, scope - else - result = Types::NIL - end - @dig_targets.resolve result, scope if @dig_targets.target? node - result - end - - def evaluate_program_node(node, scope) - evaluate node.statements, scope - end - - def evaluate_statements_node(node, scope) - if node.body.empty? - Types::NIL - else - node.body.map { evaluate _1, scope }.last - end - end - - def evaluate_def_node(node, scope) - if node.receiver - self_type = evaluate node.receiver, scope - else - current_self_types = scope.self_type.types - self_types = current_self_types.map do |type| - if type.is_a?(Types::SingletonType) && type.module_or_class.is_a?(Class) - Types::InstanceType.new type.module_or_class - else - type - end - end - self_type = Types::UnionType[*self_types] - end - if @dig_targets.dig?(node.body) || @dig_targets.dig?(node.parameters) - params_table = node.locals.to_h { [_1.to_s, Types::NIL] } - method_scope = Scope.new( - scope, - { **params_table, Scope::BREAK_RESULT => nil, Scope::NEXT_RESULT => nil, Scope::RETURN_RESULT => nil }, - self_type: self_type, - trace_lvar: false, - trace_ivar: false - ) - if node.parameters - # node.parameters is Prism::ParametersNode - assign_parameters node.parameters, method_scope, [], {} - end - - if @dig_targets.dig?(node.body) - method_scope.conditional do |s| - evaluate node.body, s - end - end - method_scope.merge_jumps - scope.update method_scope - end - Types::SYMBOL - end - - def evaluate_integer_node(_node, _scope) = Types::INTEGER - - def evaluate_float_node(_node, _scope) = Types::FLOAT - - def evaluate_rational_node(_node, _scope) = Types::RATIONAL - - def evaluate_imaginary_node(_node, _scope) = Types::COMPLEX - - def evaluate_string_node(_node, _scope) = Types::STRING - - def evaluate_x_string_node(_node, _scope) - Types::UnionType[Types::STRING, Types::NIL] - end - - def evaluate_symbol_node(_node, _scope) = Types::SYMBOL - - def evaluate_regular_expression_node(_node, _scope) = Types::REGEXP - - def evaluate_string_concat_node(node, scope) - evaluate node.left, scope - evaluate node.right, scope - Types::STRING - end - - def evaluate_interpolated_string_node(node, scope) - node.parts.each { evaluate _1, scope } - Types::STRING - end - - def evaluate_interpolated_x_string_node(node, scope) - node.parts.each { evaluate _1, scope } - Types::STRING - end - - def evaluate_interpolated_symbol_node(node, scope) - node.parts.each { evaluate _1, scope } - Types::SYMBOL - end - - def evaluate_interpolated_regular_expression_node(node, scope) - node.parts.each { evaluate _1, scope } - Types::REGEXP - end - - def evaluate_embedded_statements_node(node, scope) - node.statements ? evaluate(node.statements, scope) : Types::NIL - Types::STRING - end - - def evaluate_embedded_variable_node(node, scope) - evaluate node.variable, scope - Types::STRING - end - - def evaluate_array_node(node, scope) - Types.array_of evaluate_list_splat_items(node.elements, scope) - end - - def evaluate_hash_node(node, scope) = evaluate_hash(node, scope) - def evaluate_keyword_hash_node(node, scope) = evaluate_hash(node, scope) - def evaluate_hash(node, scope) - keys = [] - values = [] - node.elements.each do |assoc| - case assoc - when Prism::AssocNode - keys << evaluate(assoc.key, scope) - values << evaluate(assoc.value, scope) - when Prism::AssocSplatNode - next unless assoc.value # def f(**); {**} - - hash = evaluate assoc.value, scope - unless hash.is_a?(Types::InstanceType) && hash.klass == Hash - hash = method_call hash, :to_hash, [], nil, nil, scope - end - if hash.is_a?(Types::InstanceType) && hash.klass == Hash - keys << hash.params[:K] if hash.params[:K] - values << hash.params[:V] if hash.params[:V] - end - end - end - if keys.empty? && values.empty? - Types::InstanceType.new Hash - else - Types::InstanceType.new Hash, K: Types::UnionType[*keys], V: Types::UnionType[*values] - end - end - - def evaluate_parentheses_node(node, scope) - node.body ? evaluate(node.body, scope) : Types::NIL - end - - def evaluate_constant_path_node(node, scope) - type, = evaluate_constant_node_info node, scope - type - end - - def evaluate_self_node(_node, scope) = scope.self_type - - def evaluate_true_node(_node, _scope) = Types::TRUE - - def evaluate_false_node(_node, _scope) = Types::FALSE - - def evaluate_nil_node(_node, _scope) = Types::NIL - - def evaluate_source_file_node(_node, _scope) = Types::STRING - - def evaluate_source_line_node(_node, _scope) = Types::INTEGER - - def evaluate_source_encoding_node(_node, _scope) = Types::InstanceType.new(Encoding) - - def evaluate_numbered_reference_read_node(_node, _scope) - Types::UnionType[Types::STRING, Types::NIL] - end - - def evaluate_back_reference_read_node(_node, _scope) - Types::UnionType[Types::STRING, Types::NIL] - end - - def evaluate_reference_read(node, scope) - scope[node.name.to_s] || Types::NIL - end - alias evaluate_constant_read_node evaluate_reference_read - alias evaluate_global_variable_read_node evaluate_reference_read - alias evaluate_local_variable_read_node evaluate_reference_read - alias evaluate_class_variable_read_node evaluate_reference_read - alias evaluate_instance_variable_read_node evaluate_reference_read - - - def evaluate_call_node(node, scope) - is_field_assign = node.name.match?(/[^<>=!\]]=\z/) || (node.name == :[]= && !node.call_operator) - receiver_type = node.receiver ? evaluate(node.receiver, scope) : scope.self_type - evaluate_method = lambda do |scope| - args_types, kwargs_types, block_sym_node, has_block = evaluate_call_node_arguments node, scope - - if block_sym_node - block_sym = block_sym_node.value - if @dig_targets.target? block_sym_node - # method(args, &:completion_target) - call_block_proc = ->(block_args, _self_type) do - block_receiver = block_args.first || Types::OBJECT - @dig_targets.resolve block_receiver, scope - Types::OBJECT - end - else - call_block_proc = ->(block_args, _self_type) do - block_receiver, *rest = block_args - block_receiver ? method_call(block_receiver || Types::OBJECT, block_sym, rest, nil, nil, scope) : Types::OBJECT - end - end - elsif node.block.is_a? Prism::BlockNode - call_block_proc = ->(block_args, block_self_type) do - scope.conditional do |s| - numbered_parameters = node.block.locals.grep(/\A_[1-9]/).map(&:to_s) - params_table = node.block.locals.to_h { [_1.to_s, Types::NIL] } - table = { **params_table, Scope::BREAK_RESULT => nil, Scope::NEXT_RESULT => nil } - block_scope = Scope.new s, table, self_type: block_self_type, trace_ivar: !block_self_type - # TODO kwargs - if node.block.parameters&.parameters - # node.block.parameters is Prism::BlockParametersNode - assign_parameters node.block.parameters.parameters, block_scope, block_args, {} - elsif !numbered_parameters.empty? - assign_numbered_parameters numbered_parameters, block_scope, block_args, {} - end - result = node.block.body ? evaluate(node.block.body, block_scope) : Types::NIL - block_scope.merge_jumps - s.update block_scope - nexts = block_scope[Scope::NEXT_RESULT] - breaks = block_scope[Scope::BREAK_RESULT] - if block_scope.terminated? - [Types::UnionType[*nexts], breaks] - else - [Types::UnionType[result, *nexts], breaks] - end - end - end - elsif has_block - call_block_proc = ->(_block_args, _self_type) { Types::OBJECT } - end - result = method_call receiver_type, node.name, args_types, kwargs_types, call_block_proc, scope - if is_field_assign - args_types.last || Types::NIL - else - result - end - end - if node.call_operator == '&.' - result = scope.conditional { evaluate_method.call _1 } - if receiver_type.nillable? - Types::UnionType[result, Types::NIL] - else - result - end - else - evaluate_method.call scope - end - end - - def evaluate_and_node(node, scope) = evaluate_and_or(node, scope, and_op: true) - def evaluate_or_node(node, scope) = evaluate_and_or(node, scope, and_op: false) - def evaluate_and_or(node, scope, and_op:) - left = evaluate node.left, scope - right = scope.conditional { evaluate node.right, _1 } - if and_op - Types::UnionType[right, Types::NIL, Types::FALSE] - else - Types::UnionType[left, right] - end - end - - def evaluate_call_operator_write_node(node, scope) = evaluate_call_write(node, scope, :operator, node.write_name) - def evaluate_call_and_write_node(node, scope) = evaluate_call_write(node, scope, :and, node.write_name) - def evaluate_call_or_write_node(node, scope) = evaluate_call_write(node, scope, :or, node.write_name) - def evaluate_index_operator_write_node(node, scope) = evaluate_call_write(node, scope, :operator, :[]=) - def evaluate_index_and_write_node(node, scope) = evaluate_call_write(node, scope, :and, :[]=) - def evaluate_index_or_write_node(node, scope) = evaluate_call_write(node, scope, :or, :[]=) - def evaluate_call_write(node, scope, operator, write_name) - receiver_type = evaluate node.receiver, scope - if write_name == :[]= - args_types, kwargs_types, block_sym_node, has_block = evaluate_call_node_arguments node, scope - else - args_types = [] - end - if block_sym_node - block_sym = block_sym_node.value - call_block_proc = ->(block_args, _self_type) do - block_receiver, *rest = block_args - block_receiver ? method_call(block_receiver || Types::OBJECT, block_sym, rest, nil, nil, scope) : Types::OBJECT - end - elsif has_block - call_block_proc = ->(_block_args, _self_type) { Types::OBJECT } - end - method = write_name.to_s.delete_suffix('=') - left = method_call receiver_type, method, args_types, kwargs_types, call_block_proc, scope - case operator - when :and - right = scope.conditional { evaluate node.value, _1 } - Types::UnionType[right, Types::NIL, Types::FALSE] - when :or - right = scope.conditional { evaluate node.value, _1 } - Types::UnionType[left, right] - else - right = evaluate node.value, scope - method_call left, node.operator, [right], nil, nil, scope, name_match: false - end - end - - def evaluate_variable_operator_write(node, scope) - left = scope[node.name.to_s] || Types::OBJECT - right = evaluate node.value, scope - scope[node.name.to_s] = method_call left, node.operator, [right], nil, nil, scope, name_match: false - end - alias evaluate_global_variable_operator_write_node evaluate_variable_operator_write - alias evaluate_local_variable_operator_write_node evaluate_variable_operator_write - alias evaluate_class_variable_operator_write_node evaluate_variable_operator_write - alias evaluate_instance_variable_operator_write_node evaluate_variable_operator_write - - def evaluate_variable_and_write(node, scope) - right = scope.conditional { evaluate node.value, scope } - scope[node.name.to_s] = Types::UnionType[right, Types::NIL, Types::FALSE] - end - alias evaluate_global_variable_and_write_node evaluate_variable_and_write - alias evaluate_local_variable_and_write_node evaluate_variable_and_write - alias evaluate_class_variable_and_write_node evaluate_variable_and_write - alias evaluate_instance_variable_and_write_node evaluate_variable_and_write - - def evaluate_variable_or_write(node, scope) - left = scope[node.name.to_s] || Types::OBJECT - right = scope.conditional { evaluate node.value, scope } - scope[node.name.to_s] = Types::UnionType[left, right] - end - alias evaluate_global_variable_or_write_node evaluate_variable_or_write - alias evaluate_local_variable_or_write_node evaluate_variable_or_write - alias evaluate_class_variable_or_write_node evaluate_variable_or_write - alias evaluate_instance_variable_or_write_node evaluate_variable_or_write - - def evaluate_constant_operator_write_node(node, scope) - left = scope[node.name.to_s] || Types::OBJECT - right = evaluate node.value, scope - scope[node.name.to_s] = method_call left, node.operator, [right], nil, nil, scope, name_match: false - end - - def evaluate_constant_and_write_node(node, scope) - right = scope.conditional { evaluate node.value, scope } - scope[node.name.to_s] = Types::UnionType[right, Types::NIL, Types::FALSE] - end - - def evaluate_constant_or_write_node(node, scope) - left = scope[node.name.to_s] || Types::OBJECT - right = scope.conditional { evaluate node.value, scope } - scope[node.name.to_s] = Types::UnionType[left, right] - end - - def evaluate_constant_path_operator_write_node(node, scope) - left, receiver, _parent_module, name = evaluate_constant_node_info node.target, scope - right = evaluate node.value, scope - value = method_call left, node.operator, [right], nil, nil, scope, name_match: false - const_path_write receiver, name, value, scope - value - end - - def evaluate_constant_path_and_write_node(node, scope) - _left, receiver, _parent_module, name = evaluate_constant_node_info node.target, scope - right = scope.conditional { evaluate node.value, scope } - value = Types::UnionType[right, Types::NIL, Types::FALSE] - const_path_write receiver, name, value, scope - value - end - - def evaluate_constant_path_or_write_node(node, scope) - left, receiver, _parent_module, name = evaluate_constant_node_info node.target, scope - right = scope.conditional { evaluate node.value, scope } - value = Types::UnionType[left, right] - const_path_write receiver, name, value, scope - value - end - - def evaluate_constant_path_write_node(node, scope) - receiver = evaluate node.target.parent, scope if node.target.parent - value = evaluate node.value, scope - const_path_write receiver, node.target.child.name.to_s, value, scope - value - end - - def evaluate_lambda_node(node, scope) - local_table = node.locals.to_h { [_1.to_s, Types::OBJECT] } - block_scope = Scope.new scope, { **local_table, Scope::BREAK_RESULT => nil, Scope::NEXT_RESULT => nil, Scope::RETURN_RESULT => nil } - block_scope.conditional do |s| - assign_parameters node.parameters.parameters, s, [], {} if node.parameters&.parameters - evaluate node.body, s if node.body - end - block_scope.merge_jumps - scope.update block_scope - Types::PROC - end - - def evaluate_reference_write(node, scope) - scope[node.name.to_s] = evaluate node.value, scope - end - alias evaluate_constant_write_node evaluate_reference_write - alias evaluate_global_variable_write_node evaluate_reference_write - alias evaluate_local_variable_write_node evaluate_reference_write - alias evaluate_class_variable_write_node evaluate_reference_write - alias evaluate_instance_variable_write_node evaluate_reference_write - - def evaluate_multi_write_node(node, scope) - evaluated_receivers = {} - evaluate_multi_write_receiver node, scope, evaluated_receivers - value = ( - if node.value.is_a? Prism::ArrayNode - if node.value.elements.any?(Prism::SplatNode) - evaluate node.value, scope - else - node.value.elements.map do |n| - evaluate n, scope - end - end - elsif node.value - evaluate node.value, scope - else - Types::NIL - end - ) - evaluate_multi_write node, value, scope, evaluated_receivers - value.is_a?(Array) ? Types.array_of(*value) : value - end - - def evaluate_if_node(node, scope) = evaluate_if_unless(node, scope) - def evaluate_unless_node(node, scope) = evaluate_if_unless(node, scope) - def evaluate_if_unless(node, scope) - evaluate node.predicate, scope - Types::UnionType[*scope.run_branches( - -> { node.statements ? evaluate(node.statements, _1) : Types::NIL }, - -> { node.consequent ? evaluate(node.consequent, _1) : Types::NIL } - )] - end - - def evaluate_else_node(node, scope) - node.statements ? evaluate(node.statements, scope) : Types::NIL - end - - def evaluate_while_until(node, scope) - inner_scope = Scope.new scope, { Scope::BREAK_RESULT => nil } - evaluate node.predicate, inner_scope - if node.statements - inner_scope.conditional do |s| - evaluate node.statements, s - end - end - inner_scope.merge_jumps - scope.update inner_scope - breaks = inner_scope[Scope::BREAK_RESULT] - breaks ? Types::UnionType[breaks, Types::NIL] : Types::NIL - end - alias evaluate_while_node evaluate_while_until - alias evaluate_until_node evaluate_while_until - - def evaluate_break_node(node, scope) = evaluate_jump(node, scope, :break) - def evaluate_next_node(node, scope) = evaluate_jump(node, scope, :next) - def evaluate_return_node(node, scope) = evaluate_jump(node, scope, :return) - def evaluate_jump(node, scope, mode) - internal_key = ( - case mode - when :break - Scope::BREAK_RESULT - when :next - Scope::NEXT_RESULT - when :return - Scope::RETURN_RESULT - end - ) - jump_value = ( - arguments = node.arguments&.arguments - if arguments.nil? || arguments.empty? - Types::NIL - elsif arguments.size == 1 && !arguments.first.is_a?(Prism::SplatNode) - evaluate arguments.first, scope - else - Types.array_of evaluate_list_splat_items(arguments, scope) - end - ) - scope.terminate_with internal_key, jump_value - Types::NIL - end - - def evaluate_yield_node(node, scope) - evaluate_list_splat_items node.arguments.arguments, scope if node.arguments - Types::OBJECT - end - - def evaluate_redo_node(_node, scope) - scope.terminate - Types::NIL - end - - def evaluate_retry_node(_node, scope) - scope.terminate - Types::NIL - end - - def evaluate_forwarding_super_node(_node, _scope) = Types::OBJECT - - def evaluate_super_node(node, scope) - evaluate_list_splat_items node.arguments.arguments, scope if node.arguments - Types::OBJECT - end - - def evaluate_begin_node(node, scope) - return_type = node.statements ? evaluate(node.statements, scope) : Types::NIL - if node.rescue_clause - if node.else_clause - return_types = scope.run_branches( - ->{ evaluate node.rescue_clause, _1 }, - ->{ evaluate node.else_clause, _1 } - ) - else - return_types = [ - return_type, - scope.conditional { evaluate node.rescue_clause, _1 } - ] - end - return_type = Types::UnionType[*return_types] - end - if node.ensure_clause&.statements - # ensure_clause is Prism::EnsureNode - evaluate node.ensure_clause.statements, scope - end - return_type - end - - def evaluate_rescue_node(node, scope) - run_rescue = lambda do |s| - if node.reference - error_classes_type = evaluate_list_splat_items node.exceptions, s - error_types = error_classes_type.types.filter_map do - Types::InstanceType.new _1.module_or_class if _1.is_a?(Types::SingletonType) - end - error_types << Types::InstanceType.new(StandardError) if error_types.empty? - error_type = Types::UnionType[*error_types] - case node.reference - when Prism::LocalVariableTargetNode, Prism::InstanceVariableTargetNode, Prism::ClassVariableTargetNode, Prism::GlobalVariableTargetNode, Prism::ConstantTargetNode - s[node.reference.name.to_s] = error_type - when Prism::CallNode - evaluate node.reference, s - end - end - node.statements ? evaluate(node.statements, s) : Types::NIL - end - if node.consequent # begin; rescue A; rescue B; end - types = scope.run_branches( - run_rescue, - -> { evaluate node.consequent, _1 } - ) - Types::UnionType[*types] - else - run_rescue.call scope - end - end - - def evaluate_rescue_modifier_node(node, scope) - a = evaluate node.expression, scope - b = scope.conditional { evaluate node.rescue_expression, _1 } - Types::UnionType[a, b] - end - - def evaluate_singleton_class_node(node, scope) - klass_types = evaluate(node.expression, scope).types.filter_map do |type| - Types::SingletonType.new type.klass if type.is_a? Types::InstanceType - end - klass_types = [Types::CLASS] if klass_types.empty? - table = node.locals.to_h { [_1.to_s, Types::NIL] } - sclass_scope = Scope.new( - scope, - { **table, Scope::BREAK_RESULT => nil, Scope::NEXT_RESULT => nil, Scope::RETURN_RESULT => nil }, - trace_ivar: false, - trace_lvar: false, - self_type: Types::UnionType[*klass_types] - ) - result = node.body ? evaluate(node.body, sclass_scope) : Types::NIL - scope.update sclass_scope - result - end - - def evaluate_class_node(node, scope) = evaluate_class_module(node, scope, true) - def evaluate_module_node(node, scope) = evaluate_class_module(node, scope, false) - def evaluate_class_module(node, scope, is_class) - unless node.constant_path.is_a?(Prism::ConstantReadNode) || node.constant_path.is_a?(Prism::ConstantPathNode) - # Incomplete class/module `class (statement[cursor_here])::Name; end` - evaluate node.constant_path, scope - return Types::NIL - end - const_type, _receiver, parent_module, name = evaluate_constant_node_info node.constant_path, scope - if is_class - select_class_type = -> { _1.is_a?(Types::SingletonType) && _1.module_or_class.is_a?(Class) } - module_types = const_type.types.select(&select_class_type) - module_types += evaluate(node.superclass, scope).types.select(&select_class_type) if node.superclass - module_types << Types::CLASS if module_types.empty? - else - module_types = const_type.types.select { _1.is_a?(Types::SingletonType) && !_1.module_or_class.is_a?(Class) } - module_types << Types::MODULE if module_types.empty? - end - return Types::NIL unless node.body - - table = node.locals.to_h { [_1.to_s, Types::NIL] } - if !name.empty? && (parent_module.is_a?(Module) || parent_module.nil?) - value = parent_module.const_get name if parent_module&.const_defined? name - unless value - value_type = scope[name] - value = value_type.module_or_class if value_type.is_a? Types::SingletonType - end - - if value.is_a? Module - nesting = [value, []] - else - if parent_module - nesting = [parent_module, [name]] - else - parent_nesting, parent_path = scope.module_nesting.first - nesting = [parent_nesting, parent_path + [name]] - end - nesting_key = [nesting[0].__id__, nesting[1]].join('::') - nesting_value = is_class ? Types::CLASS : Types::MODULE - end - else - # parent_module == :unknown - # TODO: dummy module - end - module_scope = Scope.new( - scope, - { **table, Scope::BREAK_RESULT => nil, Scope::NEXT_RESULT => nil, Scope::RETURN_RESULT => nil }, - trace_ivar: false, - trace_lvar: false, - self_type: Types::UnionType[*module_types], - nesting: nesting - ) - module_scope[nesting_key] = nesting_value if nesting_value - result = evaluate(node.body, module_scope) - scope.update module_scope - result - end - - def evaluate_for_node(node, scope) - node.statements - collection = evaluate node.collection, scope - inner_scope = Scope.new scope, { Scope::BREAK_RESULT => nil } - ary_type = method_call collection, :to_ary, [], nil, nil, nil, name_match: false - element_types = ary_type.types.filter_map do |ary| - ary.params[:Elem] if ary.is_a?(Types::InstanceType) && ary.klass == Array - end - element_type = Types::UnionType[*element_types] - inner_scope.conditional do |s| - evaluate_write node.index, element_type, s, nil - evaluate node.statements, s if node.statements - end - inner_scope.merge_jumps - scope.update inner_scope - breaks = inner_scope[Scope::BREAK_RESULT] - breaks ? Types::UnionType[breaks, collection] : collection - end - - def evaluate_case_node(node, scope) - evaluate(node.predicate, scope) if node.predicate - # TODO - branches = node.conditions.map do |condition| - ->(s) { evaluate_case_when_condition condition, s } - end - if node.consequent - branches << ->(s) { evaluate node.consequent, s } - else - branches << ->(_s) { Types::NIL } - end - Types::UnionType[*scope.run_branches(*branches)] - end - - def evaluate_case_match_node(node, scope) - target = evaluate(node.predicate, scope) - # TODO - branches = node.conditions.map do |condition| - ->(s) { evaluate_case_in_condition target, condition, s } - end - if node.consequent - branches << ->(s) { evaluate node.consequent, s } - end - Types::UnionType[*scope.run_branches(*branches)] - end - - def evaluate_match_required_node(node, scope) - value_type = evaluate node.value, scope - evaluate_match_pattern value_type, node.pattern, scope - Types::NIL # void value - end - - def evaluate_match_predicate_node(node, scope) - value_type = evaluate node.value, scope - scope.conditional { evaluate_match_pattern value_type, node.pattern, _1 } - Types::BOOLEAN - end - - def evaluate_range_node(node, scope) - beg_type = evaluate node.left, scope if node.left - end_type = evaluate node.right, scope if node.right - elem = (Types::UnionType[*[beg_type, end_type].compact]).nonnillable - Types::InstanceType.new Range, Elem: elem - end - - def evaluate_defined_node(node, scope) - scope.conditional { evaluate node.value, _1 } - Types::UnionType[Types::STRING, Types::NIL] - end - - def evaluate_flip_flop_node(node, scope) - scope.conditional { evaluate node.left, _1 } if node.left - scope.conditional { evaluate node.right, _1 } if node.right - Types::BOOLEAN - end - - def evaluate_multi_target_node(node, scope) - # Raw MultiTargetNode, incomplete code like `a,b`, `*a`. - evaluate_multi_write_receiver node, scope, nil - Types::NIL - end - - def evaluate_splat_node(node, scope) - # Raw SplatNode, incomplete code like `*a.` - evaluate_multi_write_receiver node.expression, scope, nil if node.expression - Types::NIL - end - - def evaluate_implicit_node(node, scope) - evaluate node.value, scope - end - - def evaluate_match_write_node(node, scope) - # /(?)(?)/ =~ string - evaluate node.call, scope - locals = node.targets.map(&:name) - locals.each { scope[_1.to_s] = Types::UnionType[Types::STRING, Types::NIL] } - Types::BOOLEAN - end - - def evaluate_match_last_line_node(_node, _scope) - Types::BOOLEAN - end - - def evaluate_interpolated_match_last_line_node(node, scope) - node.parts.each { evaluate _1, scope } - Types::BOOLEAN - end - - def evaluate_pre_execution_node(node, scope) - node.statements ? evaluate(node.statements, scope) : Types::NIL - end - - def evaluate_post_execution_node(node, scope) - node.statements && @dig_targets.dig?(node.statements) ? evaluate(node.statements, scope) : Types::NIL - end - - def evaluate_alias_method_node(_node, _scope) = Types::NIL - def evaluate_alias_global_variable_node(_node, _scope) = Types::NIL - def evaluate_undef_node(_node, _scope) = Types::NIL - def evaluate_missing_node(_node, _scope) = Types::NIL - - def evaluate_call_node_arguments(call_node, scope) - # call_node.arguments is Prism::ArgumentsNode - arguments = call_node.arguments&.arguments&.dup || [] - block_arg = call_node.block.expression if call_node.block.is_a?(Prism::BlockArgumentNode) - kwargs = arguments.pop.elements if arguments.last.is_a?(Prism::KeywordHashNode) - args_types = arguments.map do |arg| - case arg - when Prism::ForwardingArgumentsNode - # `f(a, ...)` treat like splat - nil - when Prism::SplatNode - evaluate arg.expression, scope if arg.expression - nil # TODO: splat - else - evaluate arg, scope - end - end - if kwargs - kwargs_types = kwargs.map do |arg| - case arg - when Prism::AssocNode - if arg.key.is_a?(Prism::SymbolNode) - [arg.key.value, evaluate(arg.value, scope)] - else - evaluate arg.key, scope - evaluate arg.value, scope - nil - end - when Prism::AssocSplatNode - evaluate arg.value, scope if arg.value - nil - end - end.compact.to_h - end - if block_arg.is_a? Prism::SymbolNode - block_sym_node = block_arg - elsif block_arg - evaluate block_arg, scope - end - [args_types, kwargs_types, block_sym_node, !!block_arg] - end - - def const_path_write(receiver, name, value, scope) - if receiver # receiver::A = value - singleton_type = receiver.types.find { _1.is_a? Types::SingletonType } - scope.set_const singleton_type.module_or_class, name, value if singleton_type - else # ::A = value - scope.set_const Object, name, value - end - end - - def assign_required_parameter(node, value, scope) - case node - when Prism::RequiredParameterNode - scope[node.name.to_s] = value || Types::OBJECT - when Prism::MultiTargetNode - parameters = [*node.lefts, *node.rest, *node.rights] - values = value ? sized_splat(value, :to_ary, parameters.size) : [] - parameters.zip values do |n, v| - assign_required_parameter n, v, scope - end - when Prism::SplatNode - splat_value = value ? Types.array_of(value) : Types::ARRAY - assign_required_parameter node.expression, splat_value, scope if node.expression - end - end - - def evaluate_constant_node_info(node, scope) - case node - when Prism::ConstantPathNode - name = node.child.name.to_s - if node.parent - receiver = evaluate node.parent, scope - if receiver.is_a? Types::SingletonType - parent_module = receiver.module_or_class - end - else - parent_module = Object - end - if parent_module - type = scope.get_const(parent_module, [name]) || Types::NIL - else - parent_module = :unknown - type = Types::NIL - end - when Prism::ConstantReadNode - name = node.name.to_s - type = scope[name] - end - @dig_targets.resolve type, scope if @dig_targets.target? node - [type, receiver, parent_module, name] - end - - - def assign_parameters(node, scope, args, kwargs) - args = args.dup - kwargs = kwargs.dup - size = node.requireds.size + node.optionals.size + (node.rest ? 1 : 0) + node.posts.size - args = sized_splat(args.first, :to_ary, size) if size >= 2 && args.size == 1 - reqs = args.shift node.requireds.size - if node.rest - # node.rest is Prism::RestParameterNode - posts = [] - opts = args.shift node.optionals.size - rest = args - else - posts = args.pop node.posts.size - opts = args - rest = [] - end - node.requireds.zip reqs do |n, v| - assign_required_parameter n, v, scope - end - node.optionals.zip opts do |n, v| - # n is Prism::OptionalParameterNode - values = [v] - values << evaluate(n.value, scope) if n.value - scope[n.name.to_s] = Types::UnionType[*values.compact] - end - node.posts.zip posts do |n, v| - assign_required_parameter n, v, scope - end - if node.rest&.name - # node.rest is Prism::RestParameterNode - scope[node.rest.name.to_s] = Types.array_of(*rest) - end - node.keywords.each do |n| - name = n.name.to_s.delete(':') - values = [kwargs.delete(name)] - # n is Prism::OptionalKeywordParameterNode (has n.value) or Prism::RequiredKeywordParameterNode (does not have n.value) - values << evaluate(n.value, scope) if n.respond_to?(:value) - scope[name] = Types::UnionType[*values.compact] - end - # node.keyword_rest is Prism::KeywordRestParameterNode or Prism::ForwardingParameterNode or Prism::NoKeywordsParameterNode - if node.keyword_rest.is_a?(Prism::KeywordRestParameterNode) && node.keyword_rest.name - scope[node.keyword_rest.name.to_s] = Types::InstanceType.new(Hash, K: Types::SYMBOL, V: Types::UnionType[*kwargs.values]) - end - if node.block&.name - # node.block is Prism::BlockParameterNode - scope[node.block.name.to_s] = Types::PROC - end - end - - def assign_numbered_parameters(numbered_parameters, scope, args, _kwargs) - return if numbered_parameters.empty? - max_num = numbered_parameters.map { _1[1].to_i }.max - if max_num == 1 - scope['_1'] = args.first || Types::NIL - else - args = sized_splat(args.first, :to_ary, max_num) if args.size == 1 - numbered_parameters.each do |name| - index = name[1].to_i - 1 - scope[name] = args[index] || Types::NIL - end - end - end - - def evaluate_case_when_condition(node, scope) - node.conditions.each { evaluate _1, scope } - node.statements ? evaluate(node.statements, scope) : Types::NIL - end - - def evaluate_case_in_condition(target, node, scope) - pattern = node.pattern - if pattern.is_a?(Prism::IfNode) || pattern.is_a?(Prism::UnlessNode) - cond_node = pattern.predicate - pattern = pattern.statements.body.first - end - evaluate_match_pattern(target, pattern, scope) - evaluate cond_node, scope if cond_node # TODO: conditional branch - node.statements ? evaluate(node.statements, scope) : Types::NIL - end - - def evaluate_match_pattern(value, pattern, scope) - # TODO: scope.terminate_with Scope::PATTERNMATCH_BREAK, Types::NIL - case pattern - when Prism::FindPatternNode - # TODO - evaluate_match_pattern Types::OBJECT, pattern.left, scope - pattern.requireds.each { evaluate_match_pattern Types::OBJECT, _1, scope } - evaluate_match_pattern Types::OBJECT, pattern.right, scope - when Prism::ArrayPatternNode - # TODO - pattern.requireds.each { evaluate_match_pattern Types::OBJECT, _1, scope } - evaluate_match_pattern Types::OBJECT, pattern.rest, scope if pattern.rest - pattern.posts.each { evaluate_match_pattern Types::OBJECT, _1, scope } - Types::ARRAY - when Prism::HashPatternNode - # TODO - pattern.elements.each { evaluate_match_pattern Types::OBJECT, _1, scope } - if pattern.respond_to?(:rest) && pattern.rest - evaluate_match_pattern Types::OBJECT, pattern.rest, scope - end - Types::HASH - when Prism::AssocNode - evaluate_match_pattern value, pattern.value, scope if pattern.value - Types::OBJECT - when Prism::AssocSplatNode - # TODO - evaluate_match_pattern Types::HASH, pattern.value, scope - Types::OBJECT - when Prism::PinnedVariableNode - evaluate pattern.variable, scope - when Prism::PinnedExpressionNode - evaluate pattern.expression, scope - when Prism::LocalVariableTargetNode - scope[pattern.name.to_s] = value - when Prism::AlternationPatternNode - Types::UnionType[evaluate_match_pattern(value, pattern.left, scope), evaluate_match_pattern(value, pattern.right, scope)] - when Prism::CapturePatternNode - capture_type = class_or_value_to_instance evaluate_match_pattern(value, pattern.value, scope) - value = capture_type unless capture_type.types.empty? || capture_type.types == [Types::OBJECT] - evaluate_match_pattern value, pattern.target, scope - when Prism::SplatNode - value = Types.array_of value - evaluate_match_pattern value, pattern.expression, scope if pattern.expression - value - else - # literal node - type = evaluate(pattern, scope) - class_or_value_to_instance(type) - end - end - - def class_or_value_to_instance(type) - instance_types = type.types.map do |t| - t.is_a?(Types::SingletonType) ? Types::InstanceType.new(t.module_or_class) : t - end - Types::UnionType[*instance_types] - end - - def evaluate_write(node, value, scope, evaluated_receivers) - case node - when Prism::MultiTargetNode - evaluate_multi_write node, value, scope, evaluated_receivers - when Prism::CallNode - evaluated_receivers&.[](node.receiver) || evaluate(node.receiver, scope) if node.receiver - when Prism::SplatNode - evaluate_write node.expression, Types.array_of(value), scope, evaluated_receivers if node.expression - when Prism::LocalVariableTargetNode, Prism::GlobalVariableTargetNode, Prism::InstanceVariableTargetNode, Prism::ClassVariableTargetNode, Prism::ConstantTargetNode - scope[node.name.to_s] = value - when Prism::ConstantPathTargetNode - receiver = evaluated_receivers&.[](node.parent) || evaluate(node.parent, scope) if node.parent - const_path_write receiver, node.child.name.to_s, value, scope - value - end - end - - def evaluate_multi_write(node, values, scope, evaluated_receivers) - pre_targets = node.lefts - splat_target = node.rest - post_targets = node.rights - size = pre_targets.size + (splat_target ? 1 : 0) + post_targets.size - values = values.is_a?(Array) ? values.dup : sized_splat(values, :to_ary, size) - pre_pairs = pre_targets.zip(values.shift(pre_targets.size)) - post_pairs = post_targets.zip(values.pop(post_targets.size)) - splat_pairs = splat_target ? [[splat_target, Types::UnionType[*values]]] : [] - (pre_pairs + splat_pairs + post_pairs).each do |target, value| - evaluate_write target, value || Types::NIL, scope, evaluated_receivers - end - end - - def evaluate_multi_write_receiver(node, scope, evaluated_receivers) - case node - when Prism::MultiWriteNode, Prism::MultiTargetNode - targets = [*node.lefts, *node.rest, *node.rights] - targets.each { evaluate_multi_write_receiver _1, scope, evaluated_receivers } - when Prism::CallNode - if node.receiver - receiver = evaluate(node.receiver, scope) - evaluated_receivers[node.receiver] = receiver if evaluated_receivers - end - if node.arguments - node.arguments.arguments&.each do |arg| - if arg.is_a? Prism::SplatNode - evaluate arg.expression, scope - else - evaluate arg, scope - end - end - end - when Prism::SplatNode - evaluate_multi_write_receiver node.expression, scope, evaluated_receivers if node.expression - end - end - - def evaluate_list_splat_items(list, scope) - items = list.flat_map do |node| - if node.is_a? Prism::SplatNode - next unless node.expression # def f(*); [*] - - splat = evaluate node.expression, scope - array_elem, non_array = partition_to_array splat.nonnillable, :to_a - [*array_elem, *non_array] - else - evaluate node, scope - end - end.compact.uniq - Types::UnionType[*items] - end - - def sized_splat(value, method, size) - array_elem, non_array = partition_to_array value, method - values = [Types::UnionType[*array_elem, *non_array]] - values += [array_elem] * (size - 1) if array_elem && size >= 1 - values - end - - def partition_to_array(value, method) - arrays, non_arrays = value.types.partition { _1.is_a?(Types::InstanceType) && _1.klass == Array } - non_arrays.select! do |type| - to_array_result = method_call type, method, [], nil, nil, nil, name_match: false - if to_array_result.is_a?(Types::InstanceType) && to_array_result.klass == Array - arrays << to_array_result - false - else - true - end - end - array_elem = arrays.empty? ? nil : Types::UnionType[*arrays.map { _1.params[:Elem] || Types::OBJECT }] - non_array = non_arrays.empty? ? nil : Types::UnionType[*non_arrays] - [array_elem, non_array] - end - - def method_call(receiver, method_name, args, kwargs, block, scope, name_match: true) - methods = Types.rbs_methods receiver, method_name.to_sym, args, kwargs, !!block - block_called = false - type_breaks = methods.map do |method, given_params, method_params| - receiver_vars = receiver.is_a?(Types::InstanceType) ? receiver.params : {} - free_vars = method.type.free_variables - receiver_vars.keys.to_set - vars = receiver_vars.merge Types.match_free_variables(free_vars, method_params, given_params) - if block && method.block - params_type = method.block.type.required_positionals.map do |func_param| - Types.from_rbs_type func_param.type, receiver, vars - end - self_type = Types.from_rbs_type method.block.self_type, receiver, vars if method.block.self_type - block_response, breaks = block.call params_type, self_type - block_called = true - vars.merge! Types.match_free_variables(free_vars - vars.keys.to_set, [method.block.type.return_type], [block_response]) - end - if Types.method_return_bottom?(method) - [nil, breaks] - else - [Types.from_rbs_type(method.type.return_type, receiver, vars || {}), breaks] - end - end - block&.call [], nil unless block_called - terminates = !type_breaks.empty? && type_breaks.map(&:first).all?(&:nil?) - types = type_breaks.map(&:first).compact - breaks = type_breaks.map(&:last).compact - types << OBJECT_METHODS[method_name.to_sym] if name_match && OBJECT_METHODS.has_key?(method_name.to_sym) - - if method_name.to_sym == :new - receiver.types.each do |type| - if type.is_a?(Types::SingletonType) && type.module_or_class.is_a?(Class) - types << Types::InstanceType.new(type.module_or_class) - end - end - end - scope&.terminate if terminates && breaks.empty? - Types::UnionType[*types, *breaks] - end - - def self.calculate_target_type_scope(binding, parents, target) - dig_targets = DigTarget.new(parents, target) do |type, scope| - return type, scope - end - program = parents.first - scope = Scope.from_binding(binding, program.locals) - new(dig_targets).evaluate program, scope - [Types::NIL, scope] - end - end - end -end diff --git a/lib/irb/type_completion/types.rb b/lib/irb/type_completion/types.rb deleted file mode 100644 index f0f2342ff..000000000 --- a/lib/irb/type_completion/types.rb +++ /dev/null @@ -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 diff --git a/test/irb/test_context.rb b/test/irb/test_context.rb index 5804607d1..f4a19ee3c 100644 --- a/test/irb/test_context.rb +++ b/test/irb/test_context.rb @@ -666,14 +666,9 @@ def test_build_completor 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 diff --git a/test/irb/test_type_completor.rb b/test/irb/test_type_completor.rb new file mode 100644 index 000000000..cf4fc12c9 --- /dev/null +++ b/test/irb/test_type_completor.rb @@ -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 diff --git a/test/irb/type_completion/test_scope.rb b/test/irb/type_completion/test_scope.rb deleted file mode 100644 index d7f9540b0..000000000 --- a/test/irb/type_completion/test_scope.rb +++ /dev/null @@ -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 diff --git a/test/irb/type_completion/test_type_analyze.rb b/test/irb/type_completion/test_type_analyze.rb deleted file mode 100644 index ac7a65c72..000000000 --- a/test/irb/type_completion/test_type_analyze.rb +++ /dev/null @@ -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; /(?)/=~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 diff --git a/test/irb/type_completion/test_type_completor.rb b/test/irb/type_completion/test_type_completor.rb deleted file mode 100644 index f947cc4e6..000000000 --- a/test/irb/type_completion/test_type_completor.rb +++ /dev/null @@ -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 diff --git a/test/irb/type_completion/test_types.rb b/test/irb/type_completion/test_types.rb deleted file mode 100644 index 7698bd2fc..000000000 --- a/test/irb/type_completion/test_types.rb +++ /dev/null @@ -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