From 750a675c4e700245c0674ad91b826a815d2de486 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:38:36 +0200 Subject: [PATCH 1/4] Port prototype rbi generation to prism Apart from porting to prism, this also does the following: 1. Remove `variable` tracking for `type_of0`. It contains `AST::TypeParam` but checked for inclusion of a Symbol. There's no difference in the output even when this is fixed, so I just removed it entirely 2. Have the parse method return declarations, make it a class method. Just more convenient with the new structure. Eventually `rb prototype` will do this as well 3. Allow to use it on jruby/truffleruby. 4. Split comment parsing from comment processing in the helper. In rbi the comments now come from a plain parse. When `prototype rb` uses prism as well, `parse_comments` can be removed (as well as most of the other helper methods there) I tested this against code samples from https://github.com/Shopify/tapioca/blob/d029cc9c3f76865f61fdbaff68d75e18a0014764/spec/tapioca/gem/pipeline_spec.rb The output is largely the same, and improved in some areas. For example, `type_member` with no paren is no longer considered an untyped constant. Previously that was only the case for `type_member()` or when an argument was passed like `type_member(:out)`. --- lib/rbs/cli.rb | 18 +- lib/rbs/prototype/helpers.rb | 40 +- lib/rbs/prototype/rb.rb | 1 + lib/rbs/prototype/rbi.rb | 1162 ++++++++++++++++---------------- sig/cli.rbs | 2 +- sig/prototype/helpers.rbs | 2 + sig/prototype/rb.rbs | 2 +- sig/prototype/rbi.rbs | 106 ++- test/rbs/cli_test.rb | 2 +- test/rbs/rb_prototype_test.rb | 117 +--- test/rbs/rbi_prototype_test.rb | 341 +++++----- 11 files changed, 854 insertions(+), 939 deletions(-) diff --git a/lib/rbs/cli.rb b/lib/rbs/cli.rb index c645c2deb2..244f51a65d 100644 --- a/lib/rbs/cli.rb +++ b/lib/rbs/cli.rb @@ -108,7 +108,8 @@ def parse_logging_options(opts) opts end - def has_parser? + def has_parser?(format) + return true if format == "rbi" defined?(RubyVM::AbstractSyntaxTree) ? true : false end @@ -683,7 +684,7 @@ def autoload(name, path) end def run_prototype_file(format, args) - availability = unless has_parser? + availability = unless has_parser?(format) "\n** This command does not work on this interpreter (#{RUBY_ENGINE}) **\n" end @@ -728,7 +729,7 @@ def run_prototype_file(format, args) opts.parse!(args) - unless has_parser? + unless has_parser?(format) stdout.puts "Not supported on this interpreter (#{RUBY_ENGINE})." return 1 end @@ -741,7 +742,7 @@ def run_prototype_file(format, args) new_parser = -> do case format when "rbi" - Prototype::RBI.new() + Prototype::RBI when "rb" Prototype::RB.new() else @@ -796,7 +797,7 @@ def run_prototype_file(format, args) parser = new_parser[] begin - parser.parse file_path.read() + decls = parser.parse file_path.read() rescue SyntaxError stdout.puts " ⚠️ Unable to parse due to SyntaxError: `#{file_path}`" next @@ -817,7 +818,7 @@ def run_prototype_file(format, args) (output_path.parent).mkpath output_path.open("w") do |io| writer = Writer.new(out: io) - writer.write(parser.decls) + writer.write(decls) end end end @@ -837,13 +838,12 @@ def run_prototype_file(format, args) else # file mode parser = new_parser[] + writer = Writer.new(out: stdout) input_paths.each do |file| - parser.parse file.read() + writer.write parser.parse(file.read()) end - writer = Writer.new(out: stdout) - writer.write parser.decls end 0 diff --git a/lib/rbs/prototype/helpers.rb b/lib/rbs/prototype/helpers.rb index f508f45c5b..a1869559dc 100644 --- a/lib/rbs/prototype/helpers.rb +++ b/lib/rbs/prototype/helpers.rb @@ -7,24 +7,28 @@ module Helpers def parse_comments(string, include_trailing:) Prism.parse_comments(string, version: "current").yield_self do |prism_comments| # steep:ignore UnexpectedKeywordArgument - prism_comments.each_with_object({}) do |comment, hash| #$ Hash[Integer, AST::Comment] - # Skip EmbDoc comments - next unless comment.is_a?(Prism::InlineComment) - # skip like `module Foo # :nodoc:` - next if comment.trailing? && !include_trailing - - line = comment.location.start_line - body = "#{comment.location.slice}\n" - body = body[2..-1] or raise - body = "\n" if body.empty? - - comment = AST::Comment.new(string: body, location: nil) - if prev_comment = hash.delete(line - 1) - hash[line] = AST::Comment.new(string: prev_comment.string + comment.string, - location: nil) - else - hash[line] = comment - end + process_comments(prism_comments, include_trailing: include_trailing) + end + end + + def process_comments(comments, include_trailing:) + comments.each_with_object({}) do |comment, hash| #$ Hash[Integer, AST::Comment] + # Skip EmbDoc comments + next unless comment.is_a?(Prism::InlineComment) + # skip like `module Foo # :nodoc:` + next if comment.trailing? && !include_trailing + + line = comment.location.start_line + body = "#{comment.slice}\n" + body = body[2..-1] or raise + body = "\n" if body.empty? + + comment = AST::Comment.new(string: body, location: nil) + if prev_comment = hash.delete(line - 1) + hash[line] = AST::Comment.new(string: prev_comment.string + comment.string, + location: nil) + else + hash[line] = comment end end end diff --git a/lib/rbs/prototype/rb.rb b/lib/rbs/prototype/rb.rb index 8e3562db46..3711109699 100644 --- a/lib/rbs/prototype/rb.rb +++ b/lib/rbs/prototype/rb.rb @@ -77,6 +77,7 @@ def parse(string) comments = parse_comments(string, include_trailing: false) process RubyVM::AbstractSyntaxTree.parse(string), decls: source_decls, comments: comments, context: Context.initial + decls end def process(node, decls:, comments:, context:) diff --git a/lib/rbs/prototype/rbi.rb b/lib/rbs/prototype/rbi.rb index 169e910422..794212bfec 100644 --- a/lib/rbs/prototype/rbi.rb +++ b/lib/rbs/prototype/rbi.rb @@ -3,11 +3,17 @@ module RBS module Prototype class RBI - include Helpers + extend Helpers - attr_reader :decls - attr_reader :modules - attr_reader :last_sig + def self.parse(string) + parse_result = Prism.parse(string, version: "current") + raise SyntaxError unless parse_result.success? + + comments = process_comments(parse_result.comments, include_trailing: true) + visitor = Visitor.new(comments) + visitor.visit(parse_result.value) + visitor.decls + end class Context attr_accessor :singleton @@ -19,165 +25,61 @@ def initialize(singleton:, visibility:) end end - def initialize - @decls = [] - - @modules = [] - @contexts = [] - @emitted_visibility = {} - end + class Visitor < Prism::Visitor + attr_reader :decls + attr_reader :modules + attr_reader :last_sig - def parse(string) - comments = parse_comments(string, include_trailing: true) - process RubyVM::AbstractSyntaxTree.parse(string), comments: comments - end + def initialize(comments) + @comments = comments - def append_decl(decl) - if mod = current_module - mod.members << decl - else - decls << decl + @decls = [] + @modules = [] + @contexts = [] + @emitted_visibility = {} end - end - - def push_class(name, super_class, comment:) - class_decl = AST::Declarations::Class.new( - name: const_to_name(name), - super_class: super_class && AST::Declarations::Class::Super.new(name: const_to_name(super_class), args: [], location: nil), - type_params: [], - members: [], - annotations: [], - location: nil, - comment: comment - ) - - append_decl class_decl - modules << class_decl - @contexts << Context.new(singleton: false, visibility: :public) - @emitted_visibility[class_decl.object_id] = :public - - yield - ensure - @contexts.pop - modules.pop - end - def push_module(name, comment:) - module_decl = AST::Declarations::Module.new( - name: const_to_name(name), - type_params: [], - members: [], - annotations: [], - location: nil, - self_types: [], - comment: comment - ) - - append_decl module_decl - modules << module_decl - @contexts << Context.new(singleton: false, visibility: :public) - @emitted_visibility[module_decl.object_id] = :public - - yield - ensure - @contexts.pop - modules.pop - end - - def current_module - modules.last - end - - def current_module! - current_module or raise - end - - def current_context - @contexts.last - end - - def current_context! - current_context or raise - end - - # Visibility of a member, given as `private def ...` in RBS - # - # Returns `nil` for members in a visibility _section_, which `sync_visibility` emits instead. - def member_visibility(context) - # RBS visibility sections don't apply to singleton members, so they need their own visibility. - if context.singleton && context.visibility != :public - :private - end - end - - def sync_visibility(visibility) - # Visibility sections don't apply to singleton members in RBS. - return if current_context!.singleton - - # RBS has no protected visibility. Private is the conservative fallback. - visibility = :private if visibility == :protected - - mod = current_module! - return if @emitted_visibility[mod.object_id] == visibility - - member = case visibility - when :public - AST::Members::Public.new(location: nil) - when :private - AST::Members::Private.new(location: nil) - else - raise "Unexpected visibility: #{visibility}" - end - - mod.members << member - @emitted_visibility[mod.object_id] = visibility - end - - def push_sig(node) - if last_sig = @last_sig - last_sig << node - else - @last_sig = [node] + def visit_class_node(node) + comment = @comments[node.start_line - 1] + push_class node.constant_path, node.superclass, comment: comment do + visit(node.body) + end end - end - def pop_sig - @last_sig.tap do - @last_sig = nil + def visit_module_node(node) + comment = @comments[node.start_line - 1] + push_module node.constant_path, comment: comment do + visit(node.body) + end end - end - - def join_comments(nodes, comments) - cs = nodes.map {|node| comments[node.first_lineno - 1] }.compact - AST::Comment.new(string: cs.map(&:string).join("\n"), location: nil) - end - def process(node, outer: [], comments:) - case node.type - when :CLASS - comment = comments[node.first_lineno - 1] - push_class node.children[0], node.children[1], comment: comment do - process node.children[2], outer: outer + [node], comments: comments - end - when :MODULE - comment = comments[node.first_lineno - 1] - push_module node.children[0], comment: comment do - process node.children[1], outer: outer + [node], comments: comments - end - when :SCLASS - if node.children[0].type == :SELF + def visit_singleton_class_node(node) + if node.expression.is_a?(Prism::SelfNode) @contexts << Context.new(singleton: true, visibility: :public) begin - process node.children[1], outer: outer + [node], comments: comments + visit(node.body) ensure @contexts.pop end end - when :FCALL - case node.children[0] + end + + def visit_call_node(node) + return if node.receiver + arguments = node.arguments&.arguments || [] + + if node.variable_call? + case node.name + when :private, :protected, :public + current_context!.visibility = node.name + end + return + end + + case node.name when :include - each_arg node.children[1] do |arg| - if arg.type == :CONST || arg.type == :COLON2 || arg.type == :COLON3 + arguments.each do |arg| + if arg.is_a?(Prism::ConstantReadNode) || arg.is_a?(Prism::ConstantPathNode) name = const_to_name(arg) include_member = AST::Members::Include.new( name: name, @@ -190,8 +92,8 @@ def process(node, outer: [], comments:) end end when :extend - each_arg node.children[1] do |arg| - if arg.type == :CONST || arg.type == :COLON2 || arg.type == :COLON3 + arguments.each do |arg| + if arg.is_a?(Prism::ConstantReadNode) || arg.is_a?(Prism::ConstantPathNode) name = const_to_name(arg) unless ["T::Generic", "T::Helpers", "T::Sig"].include?(name.to_s.delete_prefix("::")) member = AST::Members::Extend.new( @@ -206,39 +108,55 @@ def process(node, outer: [], comments:) end end when :sig - out = outer.last or raise - push_sig out.children.last.children.last + case node.block + in Prism::BlockNode[body: Prism::StatementsNode[body: [first, *]]] + push_sig(first) + else + raise("malformed sig") + end when :attr_reader, :attr_writer, :attr_accessor - process_attribute node, comments: comments + process_attribute node, node.name when :private, :protected, :public - process_visibility node, outer: outer, comments: comments + process_visibility node, node.name when :alias_method - new, old = each_arg(node.children[1]).map {|x| x.children[0] } + case arguments + in [Prism::SymbolNode => new, Prism::SymbolNode => old] + current_module!.members << AST::Members::Alias.new( + new_name: new.value, + old_name: old.value, + location: nil, + annotations: [], + kind: current_context!.singleton ? :singleton : :instance, + comment: nil + ) + end + end + end + + def visit_alias_method_node(node) + sync_visibility(current_context!.visibility) + if node in { old_name: Prism::SymbolNode => old_name, new_name: Prism::SymbolNode => new_name } current_module!.members << AST::Members::Alias.new( - new_name: new, - old_name: old, + new_name: new_name.value, + old_name: old_name.value, location: nil, annotations: [], kind: current_context!.singleton ? :singleton : :instance, comment: nil ) end - when :VCALL - case node.children[0] - when :private, :protected, :public - current_context!.visibility = node.children[0] - end - when :DEFS - sigs = pop_sig + end - if sigs - comment = join_comments(sigs, comments) + def visit_def_node(node) + sigs = pop_sig + return unless sigs - args = node.children[2] - types = sigs.map {|sig| method_type(args, sig, variables: current_module!.type_params, overloads: sigs.size) }.compact + comment = join_comments(sigs) + if node.receiver + types = sigs.map {|sig| method_type(node.parameters, sig, overloads: sigs.size) }.compact current_module!.members << AST::Members::MethodDefinition.new( - name: node.children[1], + name: node.name, location: nil, annotations: [], overloads: types.map {|type| AST::Members::MethodDefinition::Overload.new(annotations: [], method_type: type) }, @@ -247,21 +165,14 @@ def process(node, outer: [], comments:) overloading: false, visibility: nil ) - end - - when :DEFN - sigs = pop_sig - - if sigs + else context = current_context! sync_visibility(context.visibility) - comment = join_comments(sigs, comments) - args = node.children[1] - types = sigs.map {|sig| method_type(args, sig, variables: current_module!.type_params, overloads: sigs.size) }.compact + types = sigs.map {|sig| method_type(node.parameters, sig, overloads: sigs.size) }.compact current_module!.members << AST::Members::MethodDefinition.new( - name: node.children[0], + name: node.name, location: nil, annotations: [], overloads: types.map {|type| AST::Members::MethodDefinition::Overload.new(annotations: [], method_type: type) }, @@ -271,25 +182,28 @@ def process(node, outer: [], comments:) visibility: member_visibility(context) ) end + end - when :CDECL - if (send = node.children.last) && send.type == :FCALL && send.children[0] == :type_member - unless each_arg(send.children[1]).any? {|node| - node.type == :HASH && - each_arg(node.children[0]).each_slice(2).any? {|a, _| symbol_literal_node?(a) == :fixed } - } + def visit_constant_write_node(node) + if (send = node.value).is_a?(Prism::CallNode) && !send.receiver && send.name == :type_member + arguments = send.arguments&.arguments || [] + not_fixed = arguments.none? do |node| + node.is_a?(Prism::KeywordHashNode) && + node.elements.none? { |assoc| (assoc in Prism::AssocNode[key: Prism::SymbolNode => key]) && key.value == :fixed } + end + if not_fixed # @type var variance: AST::TypeParam::variance? - if (a0 = each_arg(send.children[1]).to_a[0]) && (v = symbol_literal_node?(a0)) - variance = case v - when :out + if (first_arg = arguments.first).is_a?(Prism::SymbolNode) + variance = case first_arg.value + when "out" :covariant - when :in + when "in" :contravariant end end current_module!.type_params << AST::TypeParam.new( - name: node.children[0], + name: node.name, variance: variance || :invariant, location: nil, upper_bound: nil, @@ -298,16 +212,19 @@ def process(node, outer: [], comments:) ) end else - name = node.children[0].yield_self do |n| - n.is_a?(Symbol) ? TypeName.new(namespace: Namespace.empty, name: n) : const_to_name(n) + name = if node.is_a?(Prism::ConstantWriteNode) + TypeName.new(namespace: Namespace.empty, name: node.name) + else + const_to_name(node.target) end - value_node = node.children.last - type = if value_node && value_node.type == :CALL && value_node.children[1] == :let - type_node = each_arg(value_node.children[2]).to_a[1] - type_of type_node, variables: current_module&.type_params || [] - else - Types::Bases::Any.new(location: nil) - end + + value_node = node.value + type = if value_node.is_a?(Prism::CallNode) && value_node.name == :let + type_node = (value_node.arguments&.arguments || [])[1] + type_of type_node + else + Types::Bases::Any.new(location: nil) + end append_decl AST::Declarations::Constant.new( name: name, type: type, @@ -316,476 +233,541 @@ def process(node, outer: [], comments:) annotations: [] ) end - when :ALIAS - sync_visibility(current_context!.visibility) - current_module!.members << AST::Members::Alias.new( - new_name: node.children[0].children[0], - old_name: node.children[1].children[0], + end + alias visit_constant_path_write_node visit_constant_write_node + + def visit_constant_target_node(node) + append_decl AST::Declarations::Constant.new( + name: TypeName.new(namespace: Namespace.empty, name: node.name), + type: Types::Bases::Any.new(location: nil), location: nil, - annotations: [], - kind: current_context!.singleton ? :singleton : :instance, - comment: nil + comment: nil, + annotations: [] ) - else - each_child node do |child| - process child, outer: outer + [node], comments: comments - end end - end - def process_visibility(node, outer:, comments:) - visibility = node.children[0] - args = each_arg(node.children[1]).to_a - context = current_context! - - if args.empty? - context.visibility = visibility - else - previous_visibility = context.visibility - context.visibility = visibility - - begin - args.each do |arg| - if arg.type == :DEFN || arg.type == :DEFS - process arg, outer: outer + [node], comments: comments - end - end - ensure - context.visibility = previous_visibility + def append_decl(decl) + if mod = current_module + mod.members << decl + else + decls << decl end end - end - def process_attribute(node, comments:) - sigs = pop_sig - kind = node.children[0] - context = current_context! - sync_visibility(context.visibility) - - type = attribute_type(kind, sigs) - comment = join_comments(sigs, comments) if sigs - member_class = case kind - when :attr_reader - AST::Members::AttrReader - when :attr_writer - AST::Members::AttrWriter - when :attr_accessor - AST::Members::AttrAccessor - else - raise "Unexpected attribute kind: #{kind}" - end - - each_arg node.children[1] do |arg| - if name = symbol_literal_node?(arg) - current_module!.members << member_class.new( - name: name, - type: type, - ivar_name: nil, - kind: context.singleton ? :singleton : :instance, - annotations: [], - location: nil, - comment: comment, - visibility: member_visibility(context) - ) - end + def push_class(name, super_class, comment:) + class_decl = AST::Declarations::Class.new( + name: const_to_name(name), + super_class: super_class && AST::Declarations::Class::Super.new(name: const_to_name(super_class), args: [], location: nil), + type_params: [], + members: [], + annotations: [], + location: nil, + comment: comment + ) + + append_decl class_decl + modules << class_decl + @contexts << Context.new(singleton: false, visibility: :public) + @emitted_visibility[class_decl.object_id] = :public + + yield + ensure + @contexts.pop + modules.pop end - end - def attribute_type(kind, sigs) - any = Types::Bases::Any.new(location: nil) - return any unless sigs + def push_module(name, comment:) + module_decl = AST::Declarations::Module.new( + name: const_to_name(name), + type_params: [], + members: [], + annotations: [], + location: nil, + self_types: [], + comment: comment + ) + + append_decl module_decl + modules << module_decl + @contexts << Context.new(singleton: false, visibility: :public) + @emitted_visibility[module_decl.object_id] = :public - method_types = sigs.filter_map do |sig| - method_type(nil, sig, variables: current_module!.type_params, overloads: sigs.size) + yield + ensure + @contexts.pop + modules.pop end - function = method_types.last&.type - return any unless function.is_a?(Types::Function) - parameter_type = function.required_positionals.first&.type - return_type = function.return_type + def current_module + modules.last + end - case kind - when :attr_reader - return_type - when :attr_writer - parameter_type || return_type - when :attr_accessor - if return_type.is_a?(Types::Bases::Any) || return_type.is_a?(Types::Bases::Void) - parameter_type || any - else - return_type - end - else - any + def current_module! + current_module or raise end - end - def method_type(args_node, type_node, variables:, overloads:) - if type_node - if type_node.type == :CALL - method_type = method_type(args_node, type_node.children[0], variables: variables, overloads: overloads) or raise - else - method_type = MethodType.new( - type: Types::Function.empty(Types::Bases::Any.new(location: nil)), - block: nil, - location: nil, - type_params: [] - ) + def current_context + @contexts.last + end + + def current_context! + current_context or raise + end + + # Visibility of a member, given as `private def ...` in RBS + # + # Returns `nil` for members in a visibility _section_, which `sync_visibility` emits instead. + def member_visibility(context) + # RBS visibility sections don't apply to singleton members, so they need their own visibility. + if context.singleton && context.visibility != :public + :private end + end - name, args = case type_node.type - when :CALL - [ - type_node.children[1], - type_node.children[2] - ] - when :FCALL, :VCALL - [ - type_node.children[0], - type_node.children[1] - ] - end - - case name - when :returns - return_type = each_arg(args).to_a[0] - method_type.update(type: method_type.type.with_return_type(type_of(return_type, variables: variables))) - when :params - if args_node - parse_params(args_node, args, method_type, variables: variables, overloads: overloads) - else - vars = (node_to_hash(each_arg(args).to_a[0]) || {}).transform_values {|value| type_of(value, variables: variables) } + def sync_visibility(visibility) + # Visibility sections don't apply to singleton members in RBS. + return if current_context!.singleton - required_positionals = vars.map do |name, type| - Types::Function::Param.new(name: name, type: type) - end + # RBS has no protected visibility. Private is the conservative fallback. + visibility = :private if visibility == :protected - if method_type.type.is_a?(RBS::Types::Function) - method_type.update(type: method_type.type.update(required_positionals: required_positionals)) - else - method_type - end - end - when :type_parameters - type_params = [] #: Array[AST::TypeParam] + mod = current_module! + return if @emitted_visibility[mod.object_id] == visibility - each_arg args do |node| - if name = symbol_literal_node?(node) - type_params << AST::TypeParam.new( - name: name, - variance: :invariant, - upper_bound: nil, - lower_bound: nil, - location: nil, - default_type: nil - ) - end - end + member = case visibility + when :public + AST::Members::Public.new(location: nil) + when :private + AST::Members::Private.new(location: nil) + else + raise "Unexpected visibility: #{visibility}" + end - method_type.update(type_params: type_params) - when :void - method_type.update(type: method_type.type.with_return_type(Types::Bases::Void.new(location: nil))) - when :proc - method_type + mod.members << member + @emitted_visibility[mod.object_id] = visibility + end + + def push_sig(node) + if last_sig = @last_sig + last_sig << node else - method_type + @last_sig = [node] end end - end - def parse_params(args_node, args, method_type, variables:, overloads:) - vars = (node_to_hash(each_arg(args).to_a[0]) || {}).transform_values {|value| type_of(value, variables: variables) } - - # @type var required_positionals: Array[Types::Function::Param] - required_positionals = [] - # @type var optional_positionals: Array[Types::Function::Param] - optional_positionals = [] - # @type var rest_positionals: Types::Function::Param? - rest_positionals = nil - # @type var trailing_positionals: Array[Types::Function::Param] - trailing_positionals = [] - # @type var required_keywords: Hash[Symbol, Types::Function::Param] - required_keywords = {} - # @type var optional_keywords: Hash[Symbol, Types::Function::Param] - optional_keywords = {} - # @type var rest_keywords: Types::Function::Param? - rest_keywords = nil - - var_names = args_node.children[0] - pre_num, _pre_init, opt, _first_post, post_num, _post_init, rest, kw, kwrest, block = args_node.children[1].children - - pre_num.times.each do |i| - name = var_names[i] - type = vars[name] || Types::Bases::Any.new(location: nil) - required_positionals << Types::Function::Param.new(type: type, name: name) - end - - index = pre_num - while opt - name = var_names[index] - if (type = vars[name]) - optional_positionals << Types::Function::Param.new(type: type, name: name) + def pop_sig + @last_sig.tap do + @last_sig = nil end - index += 1 - opt = opt.children[1] end - if rest - name = var_names[index] - if (type = vars[name]) - rest_positionals = Types::Function::Param.new(type: type, name: name) - end - index += 1 + def join_comments(nodes) + cs = nodes.map {|node| @comments[node.start_line - 1] }.compact + AST::Comment.new(string: cs.map(&:string).join("\n"), location: nil) end - post_num.times do |i| - name = var_names[i+index] - if (type = vars[name]) - trailing_positionals << Types::Function::Param.new(type: type, name: name) + def process_visibility(node, visibility) + args = node.arguments&.arguments || [] + context = current_context! + + if args.empty? + context.visibility = visibility + else + previous_visibility = context.visibility + context.visibility = visibility + + begin + args.each do |arg| + if arg.is_a?(Prism::DefNode) + visit(arg) + end + end + ensure + context.visibility = previous_visibility + end end - index += 1 end - while kw - name, value = kw.children[0].children - if (type = vars[name]) - if value - optional_keywords[name] = Types::Function::Param.new(type: type, name: name) - else - required_keywords[name] = Types::Function::Param.new(type: type, name: name) + def process_attribute(node, kind) + sigs = pop_sig + context = current_context! + sync_visibility(context.visibility) + + type = attribute_type(kind, sigs) + comment = join_comments(sigs) if sigs + member_class = case kind + when :attr_reader + AST::Members::AttrReader + when :attr_writer + AST::Members::AttrWriter + when :attr_accessor + AST::Members::AttrAccessor + end + + node.arguments&.arguments&.each do |arg| + if arg in Prism::SymbolNode => parameter + current_module!.members << member_class.new( + name: parameter.value, + type: type, + ivar_name: nil, + kind: context.singleton ? :singleton : :instance, + annotations: [], + location: nil, + comment: comment, + visibility: member_visibility(context) + ) end end - - kw = kw.children[1] end - if kwrest - name = kwrest.children[0] - if (type = vars[name]) - rest_keywords = Types::Function::Param.new(type: type, name: name) + def attribute_type(kind, sigs) + any = Types::Bases::Any.new(location: nil) + return any unless sigs + + method_types = sigs.filter_map do |sig| + method_type(nil, sig, overloads: sigs.size) end - end + function = method_types.last&.type + return any unless function.is_a?(Types::Function) - method_block = nil - if block - if (type = vars[block]) - if type.is_a?(Types::Proc) - method_block = Types::Block.new(required: true, type: type.type, self_type: nil) - elsif type.is_a?(Types::Bases::Any) - method_block = Types::Block.new( - required: true, - type: Types::Function.empty(Types::Bases::Any.new(location: nil)), - self_type: nil - ) - # Handle an optional block like `T.nilable(T.proc.void)`. - elsif type.is_a?(Types::Optional) && (proc_type = type.type).is_a?(Types::Proc) - method_block = Types::Block.new(required: false, type: proc_type.type, self_type: nil) + parameter_type = function.required_positionals.first&.type + return_type = function.return_type + + case kind + when :attr_reader + return_type + when :attr_writer + parameter_type || return_type + when :attr_accessor + if return_type.is_a?(Types::Bases::Any) || return_type.is_a?(Types::Bases::Void) + parameter_type || any else - STDERR.puts "Unexpected block type: #{type}" - PP.pp args_node, STDERR - method_block = Types::Block.new( - required: true, - type: Types::Function.empty(Types::Bases::Any.new(location: nil)), - self_type: nil - ) + return_type end else - if overloads == 1 - method_block = Types::Block.new( - required: false, + any + end + end + + def method_type(args_node, type_node, overloads:) + if type_node + if type_node.is_a?(Prism::CallNode) && type_node.receiver + method_type = method_type(args_node, type_node.receiver, overloads: overloads) or raise + else + method_type = MethodType.new( type: Types::Function.empty(Types::Bases::Any.new(location: nil)), - self_type: nil + block: nil, + location: nil, + type_params: [] ) end + return method_type unless type_node.is_a?(Prism::CallNode) + + name = type_node.name + args = type_node.arguments&.arguments || [] + + case name + when :returns + return_type = args.first + method_type.update(type: method_type.type.with_return_type(type_of(return_type))) + when :params + if args_node + parse_params(args_node, args, method_type, overloads: overloads) + else + vars = keyword_args_to_hash(args.first).transform_values {|value| type_of(value) } + required_positionals = vars.map do |name, type| + Types::Function::Param.new(name: name, type: type) + end + + if method_type.type.is_a?(RBS::Types::Function) + method_type.update(type: method_type.type.update(required_positionals: required_positionals)) + else + method_type + end + end + when :type_parameters + type_params = [] #: Array[AST::TypeParam] + + args.each do |node| + if node in Prism::SymbolNode => parameter + type_params << AST::TypeParam.new( + name: parameter.value, + variance: :invariant, + upper_bound: nil, + lower_bound: nil, + location: nil, + default_type: nil + ) + end + end + + method_type.update(type_params: type_params) + when :void + method_type.update(type: method_type.type.with_return_type(Types::Bases::Void.new(location: nil))) + when :proc + method_type + else + method_type + end end end - if method_type.type.is_a?(Types::Function) - method_type.update( - type: method_type.type.update( - required_positionals: required_positionals, - optional_positionals: optional_positionals, - rest_positionals: rest_positionals, - trailing_positionals: trailing_positionals, - required_keywords: required_keywords, - optional_keywords: optional_keywords, - rest_keywords: rest_keywords - ), - block: method_block - ) - else - method_type - end - end + def parse_params(args_node, args, method_type, overloads:) + vars = keyword_args_to_hash(args.first).transform_values {|value| type_of(value) } - def type_of(type_node, variables:) - type = type_of0(type_node, variables: variables) + # @type var required_positionals: Array[Types::Function::Param] + required_positionals = args_node.requireds.filter_map do |arg| + next unless arg.is_a?(Prism::RequiredParameterNode) + type = vars[arg.name] || Types::Bases::Any.new(location: nil) + Types::Function::Param.new(type: type, name: arg.name) + end - case - when type.is_a?(Types::ClassInstance) && type.name.name == BuiltinNames::BasicObject.name.name - Types::Bases::Any.new(location: nil) - when type.is_a?(Types::ClassInstance) && type.name.to_s.delete_prefix("::") == "T::Boolean" - Types::Bases::Bool.new(location: nil) - when type.is_a?(Types::ClassInstance) && type.name.to_s.delete_prefix("::") == "T::Class" - Types::Bases::Any.new(location: nil) - else - type - end - end + # @type var optional_positionals: Array[Types::Function::Param] + optional_positionals = args_node.optionals.filter_map do |arg| + if (type = vars[arg.name]) + Types::Function::Param.new(type: type, name: arg.name) + end + end - def type_of0(type_node, variables:) - case - when type_node.type == :CONST - if variables.include?(type_node.children[0]) - Types::Variable.new(name: type_node.children[0], location: nil) - else - Types::ClassInstance.new(name: const_to_name(type_node), args: [], location: nil) + # @type var rest_positionals: Types::Function::Param? + rest_positionals = nil + if args_node in { rest: { name: Symbol => name } } + if (type = vars[name]) + rest_positionals = Types::Function::Param.new(type: type, name: name) + end end - when type_node.type == :COLON2 || type_node.type == :COLON3 - Types::ClassInstance.new(name: const_to_name(type_node), args: [], location: nil) - when call_node?(type_node, name: :[], receiver: -> (_) { true }) - # The type_node represents a type application - receiver = type_node.children[0] - if [:CONST, :COLON2, :COLON3].include?(receiver.type) && const_to_name(receiver).to_s.delete_prefix("::") == "T::Class" - return Types::Bases::Any.new(location: nil) + + # @type var trailing_positionals: Array[Types::Function::Param] + trailing_positionals = args_node.posts.filter_map do |arg| + next unless arg.is_a?(Prism::RequiredParameterNode) + if (type = vars[arg.name]) + Types::Function::Param.new(type: type, name: arg.name) + end end - type = type_of(type_node.children[0], variables: variables) - type.is_a?(Types::ClassInstance) or raise + # @type var required_keywords: Hash[Symbol, Types::Function::Param] + required_keywords = {} + # @type var optional_keywords: Hash[Symbol, Types::Function::Param] + optional_keywords = {} + args_node.keywords.each do |arg| + next unless (type = vars[arg.name]) + if arg.is_a?(Prism::RequiredParameterNode) + required_keywords[arg.name] = Types::Function::Param.new(type: type, name: arg.name) + else + optional_keywords[arg.name] = Types::Function::Param.new(type: type, name: arg.name) + end + end + + # @type var rest_keywords: Types::Function::Param? + rest_keywords = nil - each_arg(type_node.children[2]) do |arg| - type.args << type_of(arg, variables: variables) + if args_node in { keyword_rest: Prism::KeywordRestParameterNode(name: Symbol => name) } + if (type = vars[name]) + rest_keywords = Types::Function::Param.new(type: type, name: name) + end end - type - when call_node?(type_node, name: :type_parameter) - name = each_arg(type_node.children[2]).to_a[0].children[0] - Types::Variable.new(name: name, location: nil) - when call_node?(type_node, name: :any) - types = each_arg(type_node.children[2]).to_a.map {|node| type_of(node, variables: variables) } - Types::Union.new(types: types, location: nil) - when call_node?(type_node, name: :all) - types = each_arg(type_node.children[2]).to_a.map {|node| type_of(node, variables: variables) } - Types::Intersection.new(types: types, location: nil) - when call_node?(type_node, name: :untyped) - Types::Bases::Any.new(location: nil) - when call_node?(type_node, name: :nilable) - type = type_of each_arg(type_node.children[2]).to_a[0], variables: variables - Types::Optional.new(type: type, location: nil) - when call_node?(type_node, name: :self_type) - Types::Bases::Self.new(location: nil) - when call_node?(type_node, name: :attached_class) - Types::Bases::Instance.new(location: nil) - when call_node?(type_node, name: :noreturn) - Types::Bases::Bottom.new(location: nil) - when call_node?(type_node, name: :class_of) - type = type_of each_arg(type_node.children[2]).to_a[0], variables: variables - case type - when Types::ClassInstance - Types::ClassSingleton.new(name: type.name, location: nil) - else - STDERR.puts "Unexpected type for `class_of`: #{type}" - Types::Bases::Any.new(location: nil) + method_block = nil + if (block_name = args_node.block&.name) + if (type = vars[block_name]) + if type.is_a?(Types::Proc) + method_block = Types::Block.new(required: true, type: type.type, self_type: nil) + elsif type.is_a?(Types::Bases::Any) + method_block = Types::Block.new( + required: true, + type: Types::Function.empty(Types::Bases::Any.new(location: nil)), + self_type: nil + ) + # Handle an optional block like `T.nilable(T.proc.void)`. + elsif type.is_a?(Types::Optional) && (proc_type = type.type).is_a?(Types::Proc) + method_block = Types::Block.new(required: false, type: proc_type.type, self_type: nil) + else + STDERR.puts "Unexpected block type: #{type}" + PP.pp args_node, STDERR + method_block = Types::Block.new( + required: true, + type: Types::Function.empty(Types::Bases::Any.new(location: nil)), + self_type: nil + ) + end + else + if overloads == 1 + method_block = Types::Block.new( + required: false, + type: Types::Function.empty(Types::Bases::Any.new(location: nil)), + self_type: nil + ) + end + end end - when type_node.type == :ARRAY, type_node.type == :LIST - types = each_arg(type_node).map {|node| type_of(node, variables: variables) } - Types::Tuple.new(types: types, location: nil) - else - if proc_type?(type_node) - method_type = method_type(nil, type_node, variables: variables, overloads: 1) or raise - Types::Proc.new(type: method_type.type, block: nil, location: nil, self_type: nil) + + if method_type.type.is_a?(Types::Function) + method_type.update( + type: method_type.type.update( + required_positionals: required_positionals, + optional_positionals: optional_positionals, + rest_positionals: rest_positionals, + trailing_positionals: trailing_positionals, + required_keywords: required_keywords, + optional_keywords: optional_keywords, + rest_keywords: rest_keywords + ), + block: method_block + ) else - STDERR.puts "Unexpected type_node:" - PP.pp type_node, STDERR - Types::Bases::Any.new(location: nil) + method_type end end - end - - def proc_type?(type_node) - if call_node?(type_node, name: :proc) - true - else - type_node.type == :CALL && proc_type?(type_node.children[0]) - end - end - def call_node?(node, name:, receiver: -> (node) { node.type == :CONST && node.children[0] == :T }, args: -> (node) { true }) - node.type == :CALL && receiver[node.children[0]] && name == node.children[1] && args[node.children[2]] - end - - def const_to_name(node) - case node.type - when :CONST - TypeName.new(name: node.children[0], namespace: Namespace.empty) - when :COLON2 - if node.children[0] - namespace = const_to_name(node.children[0]).to_namespace - else - namespace = Namespace.empty - end + def type_of(type_node) + type = type_of0(type_node) - type_name = TypeName.new(name: node.children[1], namespace: namespace) - - case type_name.to_s.delete_prefix("::") - when "T::Array" - BuiltinNames::Array.name - when "T::Hash" - BuiltinNames::Hash.name - when "T::Range" - BuiltinNames::Range.name - when "T::Enumerator" - BuiltinNames::Enumerator.name - when "T::Enumerable" - BuiltinNames::Enumerable.name - when "T::Set" - BuiltinNames::Set.name + case + when type.is_a?(Types::ClassInstance) && type.name.name == BuiltinNames::BasicObject.name.name + Types::Bases::Any.new(location: nil) + when type.is_a?(Types::ClassInstance) && type.name.to_s.delete_prefix("::") == "T::Boolean" + Types::Bases::Bool.new(location: nil) + when type.is_a?(Types::ClassInstance) && type.name.to_s.delete_prefix("::") == "T::Class" + Types::Bases::Any.new(location: nil) else - type_name + type end - when :COLON3 - TypeName.new(name: node.children[0], namespace: Namespace.root) - else - raise "Unexpected node type: #{node.type}" end - end - def each_arg(array, &block) - if block_given? - if array&.type == :ARRAY || array&.type == :LIST - array.children.each do |arg| - if arg - yield arg + def type_of0(type_node) + case type_node + when Prism::ArrayNode + types = type_node.elements.map {|node| type_of(node) } + Types::Tuple.new(types: types, location: nil) + when Prism::ConstantReadNode, Prism::ConstantPathNode + Types::ClassInstance.new(name: const_to_name(type_node), args: [], location: nil) + when Prism::CallNode + arguments = type_node.arguments&.arguments || [] + if (receiver = type_node.receiver) in Prism::ConstantReadNode[name: :T] + case type_node.name + when :nilable + type = type_of(arguments.first) + Types::Optional.new(type: type, location: nil) + when :untyped + Types::Bases::Any.new(location: nil) + when :type_parameter + if arguments in [Prism::SymbolNode => first_arg] + Types::Variable.new(name: first_arg.value, location: nil) + else + STDERR.puts "Unexpected type_node: #{type_node.slice}" + Types::Bases::Any.new(location: nil) + end + when :all + types = arguments.map {|node| type_of(node) } + Types::Intersection.new(types: types, location: nil) + when :any + types = arguments.map {|node| type_of(node) } + Types::Union.new(types: types, location: nil) + when :class_of + type = type_of arguments.first + case type + when Types::ClassInstance + Types::ClassSingleton.new(name: type.name, location: nil) + else + STDERR.puts "Unexpected type_node: #{type_node.slice}" + Types::Bases::Any.new(location: nil) + end + when :proc + method_type = method_type(nil, type_node, overloads: 1) or raise + Types::Proc.new(type: method_type.type, block: nil, location: nil, self_type: nil) + when :attached_class + Types::Bases::Instance.new(location: nil) + when :self_type + Types::Bases::Self.new(location: nil) + when :noreturn + Types::Bases::Bottom.new(location: nil) + else + STDERR.puts "Unexpected type_node: #{type_node.slice}" + Types::Bases::Any.new(location: nil) + end + elsif receiver && type_node.name == :[] + case receiver + when Prism::ConstantReadNode, Prism::ConstantPathNode + return Types::Bases::Any.new(location: nil) if const_to_name(receiver).to_s.delete_prefix("::") == "T::Class" + end + + type = type_of(receiver) + type.is_a?(Types::ClassInstance) or raise + + arguments.each do |arg| + type.args << type_of(arg) end + + type + elsif proc_type?(type_node) + method_type = method_type(nil, type_node, overloads: 1) or raise + Types::Proc.new(type: method_type.type, block: nil, location: nil, self_type: nil) + else + STDERR.puts "Unexpected type_node: #{type_node.slice}" + Types::Bases::Any.new(location: nil) end + else + STDERR.puts "Unexpected type_node: #{type_node.slice}" + Types::Bases::Any.new(location: nil) end - else - enum_for :each_arg, array end - end - def each_child(node) - node.children.each do |child| - if child.is_a?(RubyVM::AbstractSyntaxTree::Node) - yield child + def proc_type?(type_node) + return false unless type_node.is_a?(Prism::CallNode) + + case type_node.receiver + in Prism::ConstantReadNode[name: :T] + true + else + proc_type?(type_node.receiver) end end - end - - def node_to_hash(node) - if node&.type == :HASH - # @type var hash: Hash[Symbol, untyped] - hash = {} - each_arg(node.children[0]).each_slice(2) do |var, type| - var or raise + def const_to_name(node) + case node + when Prism::ConstantReadNode, Prism::ConstantPathNode + parts = node.full_name_parts + absolute = false + if parts.first == :"" + absolute = true + parts.shift + end - if (name = symbol_literal_node?(var)) && type - hash[name] = type + name = parts.pop or raise + type_name = TypeName.new(name: name, namespace: Namespace[parts, absolute]) + + case type_name.to_s.delete_prefix("::") + when "T::Array" + BuiltinNames::Array.name + when "T::Hash" + BuiltinNames::Hash.name + when "T::Range" + BuiltinNames::Range.name + when "T::Enumerator" + BuiltinNames::Enumerator.name + when "T::Enumerable" + BuiltinNames::Enumerable.name + when "T::Set" + BuiltinNames::Set.name + else + type_name end + else + raise "Unexpected node type: #{node.type}" end + end - hash + def keyword_args_to_hash(node) + return {} unless node.is_a?(Prism::KeywordHashNode) + + node.elements.filter_map do |element| + case element + in Prism::AssocNode[key: Prism::SymbolNode] + [element.key.value.to_sym, element.value] + else + next + end + end.to_h end end end diff --git a/sig/cli.rbs b/sig/cli.rbs index c631ed8216..5b56646ac7 100644 --- a/sig/cli.rbs +++ b/sig/cli.rbs @@ -44,7 +44,7 @@ module RBS def parse_logging_options: (OptionParser) -> void - def has_parser?: () -> bool + def has_parser?: (String format) -> bool def run: (Array[String] args) -> Integer diff --git a/sig/prototype/helpers.rbs b/sig/prototype/helpers.rbs index d8bbf54cc7..634c712136 100644 --- a/sig/prototype/helpers.rbs +++ b/sig/prototype/helpers.rbs @@ -5,6 +5,8 @@ module RBS def parse_comments: (String, include_trailing: bool) -> Hash[Integer, AST::Comment] + def process_comments: (Array[Prism::Comment] comments, include_trailing: bool) -> Hash[Integer, AST::Comment] + def block_from_body: (node) -> Types::Block? def each_node: (Array[untyped] nodes) { (node) -> void } -> void diff --git a/sig/prototype/rb.rbs b/sig/prototype/rb.rbs index e575fc4ee1..7b5bda5e55 100644 --- a/sig/prototype/rb.rbs +++ b/sig/prototype/rb.rbs @@ -35,7 +35,7 @@ module RBS def decls: () -> Array[AST::Declarations::t] - def parse: (String) -> void + def parse: (String) -> Array[AST::Declarations::t] def process: (untyped node, decls: Array[AST::Declarations::t | AST::Members::t], comments: Hash[Integer, AST::Comment], context: Context) -> void diff --git a/sig/prototype/rbi.rbs b/sig/prototype/rbi.rbs index 2228f0cfb0..e82894714a 100644 --- a/sig/prototype/rbi.rbs +++ b/sig/prototype/rbi.rbs @@ -1,10 +1,14 @@ module RBS module Prototype class RBI - include Helpers + extend Helpers type visibility = :private | :protected | :public + type attribute = :attr_reader | :attr_writer | :attr_accessor + + def self.parse: (String) -> Array[AST::Declarations::t] + class Context attr_accessor singleton: bool @@ -13,92 +17,78 @@ module RBS def initialize: (singleton: bool, visibility: visibility) -> void end - attr_reader decls: Array[AST::Declarations::t] - - type module_decl = AST::Declarations::Class | AST::Declarations::Module - - # A stack representing the module nesting structure in the Ruby code - attr_reader modules: Array[module_decl] + class Visitor < Prism::Visitor + attr_reader decls: Array[AST::Declarations::t] - # Last subsequent `sig` calls - attr_reader last_sig: Array[RubyVM::AbstractSyntaxTree::Node]? + type module_decl = AST::Declarations::Class | AST::Declarations::Module - @contexts: Array[Context] + # A stack representing the module nesting structure in the Ruby code + attr_reader modules: Array[module_decl] - @emitted_visibility: Hash[Integer, visibility] + # Last subsequent `sig` calls + attr_reader last_sig: Array[Prism::Node]? - def initialize: () -> void + @contexts: Array[Context] - def parse: (String) -> void + @emitted_visibility: Hash[Integer, visibility] - def append_decl: (AST::Declarations::t decl) -> void + def initialize: (Hash[Integer, AST::Comment] comments) -> void - def push_class: ( - RubyVM::AbstractSyntaxTree::Node name, - RubyVM::AbstractSyntaxTree::Node super_class, - comment: AST::Comment? - ) { () -> void } -> void + def append_decl: (AST::Declarations::t decl) -> void - def push_module: (RubyVM::AbstractSyntaxTree::Node name, comment: AST::Comment?) { () -> void } -> void + def push_class: (Prism::Node name, Prism::Node? super_class, comment: AST::Comment?) { () -> void } -> void - # The inner most module/class definition, returns `nil` on toplevel - def current_module: () -> module_decl? + def push_module: (Prism::Node name, comment: AST::Comment?) { () -> void } -> void - # The inner most module/class definition, raises on toplevel - def current_module!: () -> module_decl + # The inner most module/class definition, returns `nil` on toplevel + def current_module: () -> module_decl? - def current_context: () -> Context? + # The inner most module/class definition, raises on toplevel + def current_module!: () -> module_decl - def current_context!: () -> Context + def current_context: () -> Context? - # Visibility of a member, given as `private def ...` in RBS - # - # Returns `nil` for members in a visibility _section_, which `sync_visibility` emits instead. - def member_visibility: (Context) -> AST::Members::visibility? + def current_context!: () -> Context - def sync_visibility: (visibility) -> void + # Visibility of a member, given as `private def ...` in RBS + # + # Returns `nil` for members in a visibility _section_, which `sync_visibility` emits instead. + def member_visibility: (Context) -> AST::Members::visibility? - # Put a `sig` call to current list. - def push_sig: (RubyVM::AbstractSyntaxTree::Node node) -> void + def sync_visibility: (visibility) -> void - # Clear the `sig` call list - def pop_sig: () -> Array[RubyVM::AbstractSyntaxTree::Node]? + # Put a `sig` call to current list. + def push_sig: (Prism::Node node) -> void - def join_comments: (Array[RubyVM::AbstractSyntaxTree::Node] nodes, Hash[Integer, AST::Comment] comments) -> AST::Comment + # Clear the `sig` call list + def pop_sig: () -> Array[Prism::Node]? - def process: (RubyVM::AbstractSyntaxTree::Node node, comments: Hash[Integer, AST::Comment], ?outer: Array[RubyVM::AbstractSyntaxTree::Node]) -> void + def join_comments: (Array[Prism::Node] nodes) -> AST::Comment - def process_visibility: (RubyVM::AbstractSyntaxTree::Node node, outer: Array[RubyVM::AbstractSyntaxTree::Node], comments: Hash[Integer, AST::Comment]) -> void + def process: (Prism::Node node) -> void - def process_attribute: (RubyVM::AbstractSyntaxTree::Node node, comments: Hash[Integer, AST::Comment]) -> void + def process_visibility: (Prism::CallNode node, visibility visibility) -> void - def attribute_type: (:attr_reader | :attr_writer | :attr_accessor kind, Array[RubyVM::AbstractSyntaxTree::Node]? sigs) -> Types::t + def process_attribute: (Prism::CallNode node, attribute kind) -> void - def method_type: (RubyVM::AbstractSyntaxTree::Node? args_node, RubyVM::AbstractSyntaxTree::Node? type_node, variables: Array[AST::TypeParam], overloads: Integer) -> MethodType? + def attribute_type: (attribute kind, Array[Prism::Node]? sigs) -> Types::t - def parse_params: (RubyVM::AbstractSyntaxTree::Node args_node, RubyVM::AbstractSyntaxTree::Node args, MethodType method_type, variables: Array[AST::TypeParam], overloads: Integer) -> MethodType + def method_type: (Prism::ParametersNode? args_node, Prism::Node? type_node, overloads: Integer) -> MethodType? - def type_of: (RubyVM::AbstractSyntaxTree::Node type_node, variables: Array[AST::TypeParam]) -> Types::t + def parse_params: (Prism::ParametersNode args_node, Array[Prism::Node] args, MethodType method_type, overloads: Integer) -> MethodType - def type_of0: (RubyVM::AbstractSyntaxTree::Node type_node, variables: Array[AST::TypeParam]) -> Types::t + def type_of: (Prism::Node type_node) -> Types::t - def proc_type?: (RubyVM::AbstractSyntaxTree::Node type_node) -> bool + def type_of0: (Prism::Node type_node) -> Types::t - def call_node?: (RubyVM::AbstractSyntaxTree::Node node, name: Symbol, ?receiver: ^(RubyVM::AbstractSyntaxTree::Node) -> bool, ?args: ^(RubyVM::AbstractSyntaxTree::Node) -> bool) -> bool + def proc_type?: (Prism::Node type_node) -> bool - # Receives a constant node and returns `TypeName` instance - def const_to_name: (RubyVM::AbstractSyntaxTree::Node node) -> TypeName + # Receives a constant node and returns `TypeName` instance + def const_to_name: (Prism::Node node) -> TypeName - # Receives `:ARRAY` or `:LIST` node and yields the child nodes. - def each_arg: (RubyVM::AbstractSyntaxTree::Node array) { (RubyVM::AbstractSyntaxTree::Node) -> void } -> void - | (RubyVM::AbstractSyntaxTree::Node array) -> Enumerator[RubyVM::AbstractSyntaxTree::Node, void] - - # Receives node and yields the child nodes. - def each_child: (RubyVM::AbstractSyntaxTree::Node node) { (RubyVM::AbstractSyntaxTree::Node) -> void } -> void - | (RubyVM::AbstractSyntaxTree::Node node) -> Enumerator[RubyVM::AbstractSyntaxTree::Node, void] - - # Receives a keyword `:HASH` node and returns hash instance. - def node_to_hash: (RubyVM::AbstractSyntaxTree::Node node) -> Hash[Symbol, RubyVM::AbstractSyntaxTree::Node]? + # Receives a keyword `:HASH` node and returns hash instance. + def keyword_args_to_hash: (Prism::Node node) -> Hash[Symbol, Prism::Node] + end end end end diff --git a/test/rbs/cli_test.rb b/test/rbs/cli_test.rb index 3d37fe9c03..d8407d6be7 100644 --- a/test/rbs/cli_test.rb +++ b/test/rbs/cli_test.rb @@ -902,7 +902,7 @@ def test_prototype_no_parser Dir.mktmpdir do |dir| with_cli do |cli| - def cli.has_parser? + def cli.has_parser?(format) false end diff --git a/test/rbs/rb_prototype_test.rb b/test/rbs/rb_prototype_test.rb index d628bec0df..afe5d5c790 100644 --- a/test/rbs/rb_prototype_test.rb +++ b/test/rbs/rb_prototype_test.rb @@ -25,9 +25,7 @@ class Bar < Struct.new(:bar) end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class Hello end @@ -61,9 +59,7 @@ def kw_req(a:) end end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class Hello def hello: (untyped a, ?::Integer b, *untyped c, untyped d, e: untyped, ?f: ::Integer, **untyped g) { (?) -> untyped } -> nil @@ -119,9 +115,7 @@ def self1() self end end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class Hello def initialize: () -> void @@ -219,9 +213,7 @@ def when_last_is_nil end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class Hello def with_return: () -> (1 | "2" | :x) @@ -257,9 +249,7 @@ def with_optional_block2(&block) end EOR - parser.parse(rb) - - assert_write parser.decls, <<~EOF + assert_write parser.parse(rb), <<~EOF class Hello def with_optional_block1: () ?{ (untyped) -> untyped } -> (untyped | nil) @@ -328,9 +318,7 @@ def with_unless end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOR + assert_write parser.parse(rb), <<-EOR class ReturnTypeWithIF def with_if: () -> (true | nil) @@ -358,9 +346,7 @@ def hello() end end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class Hello def self.hello: () -> nil end @@ -398,9 +384,7 @@ module Mod2 end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class Hello include Foo @@ -461,9 +445,7 @@ def foobar() end end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF module Hello def foo: () -> nil @@ -505,9 +487,7 @@ def prv4() end end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class Hello private @@ -551,9 +531,7 @@ def bar() end end RUBY - parser.parse(rb) - - assert_write parser.decls, <<~RBS + assert_write parser.parse(rb), <<~RBS class C private @@ -584,9 +562,7 @@ class << self end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class Hello alias a b @@ -633,9 +609,7 @@ def self.world end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF # Comments for class. # This is a comment. class Hello @@ -674,9 +648,7 @@ def hello end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class Object def hello: () -> nil end @@ -695,9 +667,7 @@ module Foo end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF module Foo VERSION: "0.1.1" @@ -719,9 +689,7 @@ module Foo end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF module Foo MAJOR: untyped @@ -747,9 +715,7 @@ def test_literal_types I = self EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF A: 1 B: ::Float @@ -772,8 +738,8 @@ def test_literal_types def test_invalid_byte_sequence_in_utf8 parser = RB.new - parser.parse('A = "\xff"') - assert_write parser.decls, "A: ::String\n" + rb = 'A = "\xff"' + assert_write parser.parse(rb), "A: ::String\n" end def test_argumentless_fcall @@ -787,9 +753,7 @@ class C end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class C end EOF @@ -805,9 +769,7 @@ class C end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class C def foo: () -> nil end @@ -829,9 +791,7 @@ class Baz end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF module Foo class Bar end @@ -855,9 +815,7 @@ def foo(x, y, z) end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class C def foo: (untyped x, untyped y, untyped z) -> untyped end @@ -884,9 +842,7 @@ def in_included end RUBY - parser.parse(rb) - - assert_write parser.decls, <<~RBS + assert_write parser.parse(rb), <<~RBS module M def not_refinements: () -> nil end @@ -908,9 +864,7 @@ def hello end RUBY - parser.parse(rb) - - assert_write parser.decls, <<~RBS + assert_write parser.parse(rb), <<~RBS class HelloWorld def self.world: (untyped str) -> untyped @@ -931,9 +885,7 @@ def message(message) end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class Hello # comment for ivar @message: untyped @@ -959,9 +911,7 @@ def foo end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF module Hello # comment for ivar @message: untyped @@ -1001,9 +951,7 @@ def message(message) end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write parser.parse(rb), <<-EOF class Hello # comment for cvar @@message: untyped @@ -1064,18 +1012,16 @@ def foo(...) end end RUBY - parser.parse(rb) - if RUBY_VERSION < '3.4' # Ruby <=3.3 generates AST without kwrest args for `...` args - assert_write parser.decls, <<~RBS + assert_write parser.parse(rb), <<~RBS module M def foo: (*untyped) ?{ (?) -> untyped } -> nil end RBS else # Ruby 3.4 generates AST with kwrest args for `...` args - assert_write parser.decls, <<~RBS + assert_write parser.parse(rb), <<~RBS module M def foo: (*untyped, **untyped) ?{ (?) -> untyped } -> nil end @@ -1090,9 +1036,8 @@ module M def foo = 42 end RUBY - parser.parse(rb) - assert_write parser.decls, <<~RBS + assert_write parser.parse(rb), <<~RBS module M def foo: () -> 42 end diff --git a/test/rbs/rbi_prototype_test.rb b/test/rbs/rbi_prototype_test.rb index c65cc535a7..e33307be84 100644 --- a/test/rbs/rbi_prototype_test.rb +++ b/test/rbs/rbi_prototype_test.rb @@ -1,64 +1,23 @@ require "test_helper" class RBS::RbiPrototypeTest < Test::Unit::TestCase - omit_on_truffle_ruby! "`RubyVM::AbstractSyntaxTree` is not available on TruffleRuby" - omit_on_jruby! "`RubyVM::AbstractSyntaxTree` is not available on JRuby" - RBI = RBS::Prototype::RBI include TestHelper - def test_1 - parser = RBI.new - - rbi = <<-EOR -class Array < Object - include Enumerable - - extend T::Generic - Elem = type_member(:out) - - sig do - type_parameters(:U).params( - arg0: T.type_parameter(:U), - foo: String, - bar: Integer, - baz: Object, - blk: T.proc.params(arg0: Elem).returns(BasicObject) - ) - .returns(T::Array[T.type_parameter(:U)]) - end - def self.[](*arg0, foo:, bar: 1, **baz, &blk); end -end - EOR - - parser.parse(rbi) - - parser.decls - - # decls = parser.decls - # pp parser.decls - end - def test_module - parser = RBI.new - rbi = <<-EOR module Foo end EOR - parser.parse(rbi) - - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF module Foo end EOF end def test_nested_module - parser = RBI.new - rbi = <<-EOR module Foo module Bar @@ -66,9 +25,7 @@ module Bar end EOR - parser.parse(rbi) - - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF module Foo module Bar end @@ -77,8 +34,6 @@ module Bar end def test_nested_module2 - parser = RBI.new - rbi = <<-EOR module Foo module ::Bar @@ -86,9 +41,7 @@ module ::Bar end EOR - parser.parse(rbi) - - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF module Foo module ::Bar end @@ -97,8 +50,6 @@ module ::Bar end def test_constant - parser = RBI.new - rbi = <<-EOR module Foo ABBR_DAYNAMES = T.let(T.unsafe(nil), Array) @@ -106,9 +57,7 @@ module Foo end EOR - parser.parse(rbi) - - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF module Foo ABBR_DAYNAMES: Array @@ -118,8 +67,6 @@ module Foo end def test_alias - parser = RBI.new - rbi = <<-EOR module Foo alias_method(:foo, :Bar) @@ -127,9 +74,7 @@ module Foo end EOR - parser.parse(rbi) - - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF module Foo alias foo Bar @@ -139,8 +84,6 @@ module Foo end def test_block_args - parser = RBI.new - rbi = <<-EOR class Hello sig do @@ -154,9 +97,7 @@ def hello(arg0, &blk); end end EOR - parser.parse(rbi) - - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class Hello def hello: [U] (U arg0) { (Elem arg0) -> untyped } -> ::Array[U] end @@ -164,16 +105,14 @@ def hello: [U] (U arg0) { (Elem arg0) -> untyped } -> ::Array[U] end def test_untyped_block - parser = RBI.new - - parser.parse(<<-EOF) + rbi = <<-EOF class File sig { params(blk: T.untyped).void } def self.split(&blk); end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class File def self.split: () { () -> untyped } -> void end @@ -181,8 +120,6 @@ def self.split: () { () -> untyped } -> void end def test_implicit_block - parser = RBI.new - rbi = <<-EOR class Hello sig do @@ -192,9 +129,7 @@ def hello(arg0, &blk); end end EOR - parser.parse(rbi) - - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class Hello def hello: (String arg0) ?{ () -> untyped } -> void end @@ -202,26 +137,57 @@ def hello: (String arg0) ?{ () -> untyped } -> void end def test_optional_block - parser = RBI.new - - parser.parse(<<-EOF) + rbi = <<-EOF class File sig { params(blk: T.nilable(T.proc.void)).void } def self.split(&blk); end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class File def self.split: () ?{ () -> void } -> void end EOF end - def test_overloading - parser = RBI.new + def test_various_parameters + rbi = <<-EOF +class Test + sig { params(req_pos: String, opt_pos: String, pos_splat: Object, post_pos: String).void } + def positional(req_pos, opt_pos = "", *pos_splat, post_pos); end + + sig { params(kw_req: String, kw_opt: String, kw_splat: Hash).void } + def keywords(kw_req:, kw_opt: "", **kw_splat); end +end + EOF + + assert_write RBI.parse(rbi), <<-EOF +class Test + def positional: (String req_pos, ?String opt_pos, *Object pos_splat, String post_pos) -> void + + def keywords: (?kw_req: String kw_req, ?kw_opt: String kw_opt, **Hash kw_splat) -> void +end + EOF + end - parser.parse(<<-EOF) + def test_anonymous_parameters_are_ignored + rbi = <<-EOF +class Test + def foo(a, *, b:, **, &); end + + def foo(...); end +end + EOF + + assert_write RBI.parse(rbi), <<-EOF +class Test +end + EOF + end + + def test_overloading + rbi = <<-EOF class Class sig {void} sig do @@ -248,7 +214,7 @@ def initialize(superclass=_, &blk); end EOF # Maybe, the argument `superclass` does not look like an optional parameter, but cannot detect if it is required or optional. - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class Class def initialize: () -> void | (?Class superclass) -> void @@ -259,9 +225,7 @@ def initialize: () -> void end def test_tuple - parser = RBI.new - - parser.parse(<<-EOF) + rbi = <<-EOF class File sig do params( @@ -273,7 +237,7 @@ def self.split(file); end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class File def self.split: (String file) -> [ String, String ] end @@ -281,9 +245,7 @@ def self.split: (String file) -> [ String, String ] end def test_all - parser = RBI.new - - parser.parse(<<-EOF) + rbi = <<-EOF class File sig do params( @@ -295,7 +257,7 @@ def self.split(file); end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class File def self.split: (String & Integer file) -> void end @@ -303,16 +265,14 @@ def self.split: (String & Integer file) -> void end def test_self_type - parser = RBI.new - - parser.parse(<<-EOF) + rbi = <<-EOF class File sig { returns(T.self_type) } def self.split; end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class File def self.split: () -> self end @@ -320,9 +280,7 @@ def self.split: () -> self end def test_colon - parser = RBI.new - - parser.parse(<<-EOF) + rbi = <<-EOF class Test sig { returns(Foo) } def m1; end @@ -338,7 +296,7 @@ def m4; end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class Test def m1: () -> Foo @@ -352,16 +310,14 @@ def m4: () -> ::Foo::Bar end def test_attached_class - parser = RBI.new - - parser.parse(<<-EOF) + rbi = <<-EOF class File sig { returns(T.attached_class) } def self.split; end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class File def self.split: () -> instance end @@ -369,9 +325,7 @@ def self.split: () -> instance end def test_noreturn - parser = RBI.new - - parser.parse(<<-EOF) + rbi = <<-EOF class File sig do params( @@ -383,7 +337,7 @@ def self.split(file); end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class File def self.split: (String & Integer file) -> bot end @@ -391,9 +345,7 @@ def self.split: (String & Integer file) -> bot end def test_class_of - parser = RBI.new - - parser.parse(<<-EOF) + rbi = <<-EOF class Foo sig do returns(T.class_of(String)) @@ -402,7 +354,7 @@ def foo; end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class Foo def foo: () -> singleton(String) end @@ -410,9 +362,7 @@ def foo: () -> singleton(String) end def test_parameter - parser = RBI.new - - parser.parse <<-EOF + rbi = <<-EOF class Array include Enumerable @@ -421,7 +371,7 @@ class Array end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class Array[out Elem] include Enumerable end @@ -429,16 +379,14 @@ class Array[out Elem] end def test_basic_object - parser = RBI.new - - parser.parse <<-EOF + rbi = <<-EOF class Foo sig { returns(BasicObject) } def hello; end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class Foo def hello: () -> untyped end @@ -446,16 +394,14 @@ def hello: () -> untyped end def test_bool - parser = RBI.new - - parser.parse <<-EOF + rbi = <<-EOF class Foo sig { returns(T::Boolean) } def hello; end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class Foo def hello: () -> bool end @@ -463,9 +409,7 @@ def hello: () -> bool end def test_comment - parser = RBI.new - - parser.parse <<-EOF + rbi = <<-EOF # This is a class. # # It is super useful. @@ -486,7 +430,7 @@ def self.foo; end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF # This is a class. # # It is super useful. @@ -507,9 +451,7 @@ def self.foo: () -> void end def test_non_parameter_type_member - parser = RBI.new - - parser.parse <<-EOF + rbi = <<-EOF class Dir extend T::Generic @@ -518,7 +460,7 @@ class Dir end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class Dir include Enumerable end @@ -526,31 +468,59 @@ class Dir end def test_parameter_type_member_variance - parser = RBI.new - - parser.parse <<-EOF + rbi = <<-EOF class Dir extend T::Generic X = type_member(:out) Y = type_member(:in) Z = type_member() + Elem = type_member include Enumerable end EOF - assert_write parser.decls, <<-EOF -class Dir[out X, in Y, Z] + assert_write RBI.parse(rbi), <<-EOF +class Dir[out X, in Y, Z, Elem] include Enumerable end EOF end - def test_nested_declarations_preserve_lexical_resolution - parser = RBI.new + def test_parameter_type_member_as_param + rbi = <<-EOR +class Array < Object + include Enumerable + + extend T::Generic + Elem = type_member(:out) + + sig do + type_parameters(:U).params( + arg0: T.type_parameter(:U), + foo: String, + bar: Integer, + baz: Object, + blk: T.proc.params(arg0: Elem).returns(BasicObject) + ) + .returns(T::Array[T.type_parameter(:U)]) + end + def self.[](*arg0, foo:, bar: 1, **baz, &blk); end +end + EOR - parser.parse <<-EOF + assert_write RBI.parse(rbi), <<-EOF +class Array[out Elem] < Object + include Enumerable + + def self.[]: [U] (*U arg0, ?foo: String foo, ?bar: Integer bar, **Object baz) { (Elem arg0) -> untyped } -> ::Array[U] +end + EOF + end + + def test_nested_declarations_preserve_lexical_resolution + rbi = <<-EOF module Demo class Parent; end module Helpers; end @@ -565,7 +535,7 @@ def convert(value); end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF module Demo class Parent end @@ -585,10 +555,21 @@ def convert: (Value value) -> Value EOF end - def test_nested_constant - parser = RBI.new + def test_include_with_receiver_is_ignored + rbi = <<-EOF +module Foo + Bar.singleton_class.include(Foo) +end + EOF - parser.parse <<-EOF + assert_write RBI.parse(rbi), <<-EOF +module Foo +end + EOF + end + + def test_nested_constant + rbi = <<-EOF module Demo module Modes VALUE = T.let(:value, Symbol) @@ -596,7 +577,7 @@ module Modes end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF module Demo module Modes VALUE: Symbol @@ -606,16 +587,14 @@ module Modes end def test_ignores_t_helpers - parser = RBI.new - - parser.parse <<-EOF + rbi = <<-EOF module Factory extend T::Helpers extend OtherHelpers end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF module Factory extend OtherHelpers end @@ -623,9 +602,7 @@ module Factory end def test_t_class_falls_back_to_untyped - parser = RBI.new - - parser.parse <<-EOF + rbi = <<-EOF module Factory sig do type_parameters(:Config) @@ -636,7 +613,7 @@ def make(config_class); end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF module Factory def make: [Config] (untyped config_class) -> Config end @@ -644,9 +621,7 @@ def make: [Config] (untyped config_class) -> Config end def test_singleton_class_method - parser = RBI.new - - parser.parse <<-EOF + rbi = <<-EOF class Registry class << self sig { returns(T.attached_class) } @@ -655,7 +630,7 @@ def build; end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class Registry def self.build: () -> instance end @@ -663,9 +638,7 @@ def self.build: () -> instance end def test_typed_attribute_consumes_signature - parser = RBI.new - - parser.parse <<-EOF + rbi = <<-EOF class Cache sig { returns(T.nilable(Integer)) } attr_reader :size @@ -681,7 +654,7 @@ def initialize(size: nil); end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class Cache attr_reader size: Integer? @@ -695,9 +668,7 @@ def initialize: (?size: Integer? size) -> void end def test_method_visibility - parser = RBI.new - - parser.parse <<-EOF + rbi = <<-EOF module Factory private @@ -711,7 +682,7 @@ def make; end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF module Factory private @@ -724,10 +695,34 @@ def make: () -> void EOF end - def test_singleton_method_visibility - parser = RBI.new + def test_inline_visibility + rbi = <<-EOF +class Cache + sig { params(value: Integer).void } + private def foo(value) + end - parser.parse <<-EOF + sig { params(value: Integer).void } + def bar(value) + end +end + EOF + + assert_write RBI.parse(rbi), <<-EOF +class Cache + private + + def foo: (Integer value) -> void + + public + + def bar: (Integer value) -> void +end + EOF + end + + def test_singleton_method_visibility + rbi = <<-EOF class Registry private @@ -752,7 +747,7 @@ def internal; end end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class Registry private @@ -770,9 +765,7 @@ def setup: () -> void end def test_generated_reproduction_can_be_loaded - parser = RBI.new - - parser.parse <<-EOF + rbi = <<-EOF module Demo class Parent; end module Helpers; end @@ -819,7 +812,7 @@ def helper; end EOF out = StringIO.new - RBS::Writer.new(out: out).write(parser.decls) + RBS::Writer.new(out: out).write(RBI.parse(rbi)) refute_match(/\bT::/, out.string) SignatureManager.new do |manager| @@ -838,15 +831,13 @@ def helper; end end def test_masgn - parser = RBI.new - - parser.parse <<-EOF + rbi = <<-EOF class Test A, B, C = [1, 2, 3] end EOF - assert_write parser.decls, <<-EOF + assert_write RBI.parse(rbi), <<-EOF class Test A: untyped From b1364be8647da5ef219f5444494036f9c5b6b2e3 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:04:33 +0200 Subject: [PATCH 2/4] Port prototype rb generation to prism This just leaves the runtime on rubyvm, which is deprecated and I won't port. Because of that, some helper methods are duplicated to work with both prism and rubyvm. I tested this on rails, and out of 3435 files only 28 differ. That is mainly for string literals that are divided by line continutions (previously they were embeded, now it's only saying it a string) Also some block optionality is changed for the better. For example `(callable || block || :itself.to_proc).call` now makes the block optional --- lib/rbs/cli.rb | 18 +- lib/rbs/prototype/helpers.rb | 137 ----- lib/rbs/prototype/node_usage.rb | 97 ++-- lib/rbs/prototype/rb.rb | 988 +++++++++++++++++--------------- lib/rbs/prototype/runtime.rb | 110 ++++ sig/cli.rbs | 2 - sig/prototype/helpers.rbs | 24 - sig/prototype/node_usage.rbs | 14 +- sig/prototype/rb.rbs | 80 +-- sig/prototype/runtime.rbs | 10 + test/rbs/cli_test.rb | 32 +- test/rbs/node_usage_test.rb | 9 +- test/rbs/rb_prototype_test.rb | 271 +++++---- 13 files changed, 904 insertions(+), 888 deletions(-) diff --git a/lib/rbs/cli.rb b/lib/rbs/cli.rb index 244f51a65d..8a53e93014 100644 --- a/lib/rbs/cli.rb +++ b/lib/rbs/cli.rb @@ -108,11 +108,6 @@ def parse_logging_options(opts) opts end - def has_parser?(format) - return true if format == "rbi" - defined?(RubyVM::AbstractSyntaxTree) ? true : false - end - def run(args) @original_args = args.dup @@ -684,10 +679,6 @@ def autoload(name, path) end def run_prototype_file(format, args) - availability = unless has_parser?(format) - "\n** This command does not work on this interpreter (#{RUBY_ENGINE}) **\n" - end - # @type var output_dir: Pathname? output_dir = nil # @type var base_dir: Pathname? @@ -698,7 +689,7 @@ def run_prototype_file(format, args) opts = OptionParser.new opts.banner = < 0 - diff.times do - function.required_positionals << Types::Function::Param.new( - type: untyped, - name: nil - ) - end - end - - if keywords - keywords.children[0].children.each_slice(2) do |key_node, value_node| - if key_node - key = key_node.children[0] - function.required_keywords[key] ||= - Types::Function::Param.new( - type: untyped, - name: nil - ) - end - end - end - end - else - function = Types::UntypedFunction.new(return_type: untyped) - end - - - Types::Block.new(required: required, type: function, self_type: nil) - end - end - - def each_child(node, &block) - each_node node.children, &block - end - - def each_node(nodes) - nodes.each do |child| - if child.is_a?(RubyVM::AbstractSyntaxTree::Node) - yield child - end - end - end - - def any_node?(node, nodes: [], &block) - if yield(node) - nodes << node - end - - each_child node do |child| - any_node? child, nodes: nodes, &block - end - - nodes.empty? ? nil : nodes - end - - def keyword_hash?(node) - if node && node.type == :HASH - node.children[0].children.compact.each_slice(2).all? {|key, _| - symbol_literal_node?(key) - } - else - false - end - end - - # NOTE: args_node may be a nil by a bug - # https://bugs.ruby-lang.org/issues/17495 - def args_from_node(args_node) - args_node&.children || [0, nil, nil, nil, 0, nil, nil, nil, nil, nil] - end - - def symbol_literal_node?(node) - case node.type - when :LIT - if node.children[0].is_a?(Symbol) - node.children[0] - end - when :SYM - node.children[0] - end - end - - def untyped - @untyped ||= Types::Bases::Any.new(location: nil) - end end end end diff --git a/lib/rbs/prototype/node_usage.rb b/lib/rbs/prototype/node_usage.rb index 2c5f07e0ba..09f810869d 100644 --- a/lib/rbs/prototype/node_usage.rb +++ b/lib/rbs/prototype/node_usage.rb @@ -27,69 +27,54 @@ def calculate(node, conditional:) conditional_nodes << node end - case node.type - when :IF, :UNLESS - cond_node, true_node, false_node = node.children - calculate(cond_node, conditional: true) - calculate(true_node, conditional: conditional) if true_node - calculate(false_node, conditional: conditional) if false_node - when :AND, :OR - left, right = node.children - calculate(left, conditional: true) - calculate(right, conditional: conditional) - when :QCALL - receiver, _, args = node.children - calculate(receiver, conditional: true) - calculate(args, conditional: false) if args - when :WHILE - cond, body = node.children - calculate(cond, conditional: true) - calculate(body, conditional: false) if body - when :OP_ASGN_OR, :OP_ASGN_AND - var, _, asgn = node.children - calculate(var, conditional: true) - calculate(asgn, conditional: conditional) - when :LASGN, :IASGN, :GASGN - _, lhs = node.children - calculate(lhs, conditional: conditional) if lhs - when :MASGN - lhs, _ = node.children - calculate(lhs, conditional: conditional) - when :CDECL - if node.children.size == 2 - _, lhs = node.children - calculate(lhs, conditional: conditional) - else - const, _, lhs = node.children - calculate(const, conditional: false) - calculate(lhs, conditional: conditional) - end - when :SCOPE - _, _, body = node.children - calculate(body, conditional: conditional) - when :CASE2 - _, *branches = node.children - branches.each do |branch| - if branch.type == :WHEN - list, body = branch.children - list.children.each do |child| - if child - calculate(child, conditional: true) - end - end - calculate(body, conditional: conditional) - else - calculate(branch, conditional: conditional) + case node + in Prism::IfNode + calculate(node.predicate, conditional: true) + calculate(node.statements, conditional: conditional) if node.statements + calculate(node.subsequent, conditional: conditional) if node.subsequent + in Prism::UnlessNode + calculate(node.predicate, conditional: true) + calculate(node.statements, conditional: conditional) if node.statements + calculate(node.else_clause, conditional: conditional) if node.else_clause + in Prism::AndNode | Prism::OrNode + calculate(node.left, conditional: true) + calculate(node.right, conditional: conditional) + in Prism::CallNode if node.safe_navigation? + calculate(node.receiver, conditional: true) if node.receiver + calculate(node.arguments, conditional: false) if node.arguments + in Prism::WhileNode + calculate(node.predicate, conditional: true) + calculate(node.statements, conditional: false) if node.statements + in Prism::ConstantOrWriteNode | Prism::ConstantAndWriteNode | + Prism::GlobalVariableOrWriteNode | Prism::GlobalVariableAndWriteNode | + Prism::InstanceVariableOrWriteNode | Prism::InstanceVariableAndWriteNode | + Prism::LocalVariableOrWriteNode | Prism::LocalVariableAndWriteNode + conditional_nodes << node + calculate(node.value, conditional: conditional) + in Prism::ConstantWriteNode | Prism::MultiWriteNode | + Prism::LocalVariableWriteNode | Prism::InstanceVariableWriteNode | Prism::GlobalVariableWriteNode + calculate(node.value, conditional: conditional) + in Prism::ConstantPathWriteNode + calculate(node.target, conditional: false) + calculate(node.value, conditional: conditional) + in Prism::BlockNode | Prism::ClassNode | Prism::DefNode | Prism::LambdaNode | Prism::ModuleNode | Prism::SingletonClassNode + # Anything with locals + calculate(node.body, conditional: conditional) if node.body + in Prism::CaseNode[predicate: predicate] unless predicate + node.conditions.each do |when_node| + when_node.conditions.each do |child| + calculate(child, conditional: true) end + calculate(when_node.statements, conditional: conditional) if when_node.statements end - when :BLOCK - *nodes, last = node.children + in Prism::StatementsNode + *nodes, last = node.body nodes.each do |no| calculate(no, conditional: false) end calculate(last, conditional: conditional) if last else - each_child(node) do |child| + node.compact_child_nodes.each do |child| calculate(child, conditional: false) end end diff --git a/lib/rbs/prototype/rb.rb b/lib/rbs/prototype/rb.rb index 3711109699..ad7a7218bc 100644 --- a/lib/rbs/prototype/rb.rb +++ b/lib/rbs/prototype/rb.rb @@ -3,7 +3,43 @@ module RBS module Prototype class RB - include Helpers + extend Helpers + + def self.parse(string) + parse_result = Prism.parse(string, version: "current") + raise SyntaxError unless parse_result.success? + + comments = process_comments(parse_result.comments, include_trailing: false) + visitor = Visitor.new(comments) + visitor.visit(parse_result.value) + process_decls(visitor.decls) + end + + def self.process_decls(decls) + # @type var processed_decls: Array[AST::Declarations::t] + processed_decls = [] + + # @type var top_decls: Array[AST::Declarations::t] + # @type var top_members: Array[AST::Members::t] + top_decls, top_members = _ = decls.partition {|decl| decl.is_a?(AST::Declarations::Base) } + + processed_decls.push(*top_decls) + + unless top_members.empty? + top = AST::Declarations::Class.new( + name: TypeName.new(name: :Object, namespace: Namespace.empty), + super_class: nil, + members: top_members, + annotations: [], + comment: nil, + location: nil, + type_params: [] + ) + processed_decls << top + end + + processed_decls + end class Context < Struct.new(:module_function, :singleton, :namespace, :in_def, keyword_init: true) # @implements Context @@ -39,52 +75,19 @@ def update(module_function: self.module_function, singleton: self.singleton, in_ end end - attr_reader :source_decls - attr_reader :toplevel_members - - def initialize - @source_decls = [] - end - - def decls - # @type var decls: Array[AST::Declarations::t] - decls = [] - - # @type var top_decls: Array[AST::Declarations::t] - # @type var top_members: Array[AST::Members::t] - top_decls, top_members = _ = source_decls.partition {|decl| decl.is_a?(AST::Declarations::Base) } - - decls.push(*top_decls) + class Visitor < Prism::Visitor + attr_reader :context + attr_reader :comments + attr_reader :decls - unless top_members.empty? - top = AST::Declarations::Class.new( - name: TypeName.new(name: :Object, namespace: Namespace.empty), - super_class: nil, - members: top_members, - annotations: [], - comment: nil, - location: nil, - type_params: [] - ) - decls << top + def initialize(comments) + @comments = comments + @decls = [] + @context = Context.initial end - decls - end - - def parse(string) - # @type var comments: Hash[Integer, AST::Comment] - comments = parse_comments(string, include_trailing: false) - - process RubyVM::AbstractSyntaxTree.parse(string), decls: source_decls, comments: comments, context: Context.initial - decls - end - - def process(node, decls:, comments:, context:) - case node.type - when :CLASS - class_name, super_class_node, *class_body = node.children - super_class_name = const_to_name(super_class_node, context: context) + def visit_class_node(node) + super_class_name = const_to_name(node.superclass, context: context) super_class = if super_class_name AST::Declarations::Class::Super.new(name: super_class_name, args: [], location: nil) @@ -93,87 +96,82 @@ def process(node, decls:, comments:, context:) nil end kls = AST::Declarations::Class.new( - name: const_to_name!(class_name), + name: const_to_name!(node.constant_path), super_class: super_class, type_params: [], members: [], annotations: [], location: nil, - comment: comments[node.first_lineno - 1] + comment: comments[node.start_line - 1] ) decls.push kls new_ctx = context.enter_namespace(kls.name.to_namespace) - each_node class_body do |child| - process child, decls: kls.members, comments: comments, context: new_ctx + with(decls: kls.members, context: new_ctx) do + visit(node.body) end remove_unnecessary_accessibility_methods! kls.members sort_members! kls.members + end - when :MODULE - module_name, *module_body = node.children - + def visit_module_node(node) mod = AST::Declarations::Module.new( - name: const_to_name!(module_name), + name: const_to_name!(node.constant_path), type_params: [], self_types: [], members: [], annotations: [], location: nil, - comment: comments[node.first_lineno - 1] + comment: comments[node.start_line - 1] ) decls.push mod new_ctx = context.enter_namespace(mod.name.to_namespace) - each_node module_body do |child| - process child, decls: mod.members, comments: comments, context: new_ctx + with(decls: mod.members, context: new_ctx) do + visit(node.body) end + remove_unnecessary_accessibility_methods! mod.members sort_members! mod.members + end - when :SCLASS - this, body = node.children - - if this.type != :SELF - RBS.logger.warn "`class <<` syntax with not-self may be compiled to incorrect code: #{this}" + def visit_singleton_class_node(node) + unless node.expression.is_a?(Prism::SelfNode) + RBS.logger.warn "`class <<` syntax with not-self may be compiled to incorrect code: #{node.expression.slice}" end accessibility = current_accessibility(decls) ctx = Context.initial.tap { |ctx| ctx.singleton = true } - process_children(body, decls: decls, comments: comments, context: ctx) + with(decls: decls, context: ctx) do + visit(node.body) + end decls << accessibility + end - when :DEFN, :DEFS + def visit_def_node(node) # @type var kind: Context::method_kind - - if node.type == :DEFN - def_name, def_body = node.children - kind = context.method_kind - else - _, def_name, def_body = node.children - kind = :singleton - end + kind = node.receiver ? :singleton : context.method_kind types = [ MethodType.new( type_params: [], - type: function_type_from_body(def_body, def_name), - block: block_from_body(def_body), + type: function_type_from_def_node(node), + block: block_from_body(node.body, node.parameters), location: nil ) ] member = AST::Members::MethodDefinition.new( - name: def_name, + name: node.name, location: nil, annotations: [], overloads: types.map {|type| AST::Members::MethodDefinition::Overload.new(annotations: [], method_type: type )}, kind: kind, - comment: comments[node.first_lineno - 1], + comment: comments[node.start_line - 1], overloading: false, visibility: nil ) @@ -181,27 +179,35 @@ def process(node, decls:, comments:, context:) decls.push member unless decls.include?(member) new_ctx = context.update(singleton: kind == :singleton, in_def: true) - each_node def_body.children do |child| - process child, decls: decls, comments: comments, context: new_ctx + with(decls: decls, context: new_ctx) do + visit(node.body) end + end - when :ALIAS - new_name, old_name = node.children.map { |c| literal_to_symbol(c) } - member = AST::Members::Alias.new( - new_name: new_name, - old_name: old_name, - kind: context.singleton ? :singleton : :instance, - annotations: [], - location: nil, - comment: comments[node.first_lineno - 1], - ) - decls.push member unless decls.include?(member) + def visit_alias_method_node(node) + new_name = literal_to_symbol(node.new_name) + old_name = literal_to_symbol(node.old_name) + if new_name && old_name + member = AST::Members::Alias.new( + new_name: new_name, + old_name: old_name, + kind: context.singleton ? :singleton : :instance, + annotations: [], + location: nil, + comment: comments[node.start_line - 1], + ) + decls.push member unless decls.include?(member) + end + end + + def visit_call_node(node) + visit(node.receiver) + return if node.block || node.receiver - when :FCALL, :VCALL # Inside method definition cannot reach here. - args = node.children[1]&.children || [] + args = node.arguments&.arguments || [] - case node.children[0] + case node.name when :include args.each do |arg| if (name = const_to_name(arg, context: context)) @@ -211,7 +217,7 @@ def process(node, decls:, comments:, context:) args: [], annotations: [], location: nil, - comment: comments[node.first_lineno - 1] + comment: comments[node.start_line - 1] ) end end @@ -223,7 +229,7 @@ def process(node, decls:, comments:, context:) args: [], annotations: [], location: nil, - comment: comments[node.first_lineno - 1] + comment: comments[node.start_line - 1] ) end end @@ -235,34 +241,34 @@ def process(node, decls:, comments:, context:) args: [], annotations: [], location: nil, - comment: comments[node.first_lineno - 1] + comment: comments[node.start_line - 1] ) end end when :attr_reader args.each do |arg| - if arg && (name = literal_to_symbol(arg)) + if (name = literal_to_symbol(arg)) decls << AST::Members::AttrReader.new( name: name, ivar_name: nil, type: Types::Bases::Any.new(location: nil), kind: context.attribute_kind, location: nil, - comment: comments[node.first_lineno - 1], + comment: comments[node.start_line - 1], annotations: [] ) end end when :attr_accessor args.each do |arg| - if arg && (name = literal_to_symbol(arg)) + if (name = literal_to_symbol(arg)) decls << AST::Members::AttrAccessor.new( name: name, ivar_name: nil, type: Types::Bases::Any.new(location: nil), kind: context.attribute_kind, location: nil, - comment: comments[node.first_lineno - 1], + comment: comments[node.start_line - 1], annotations: [] ) end @@ -276,20 +282,20 @@ def process(node, decls:, comments:, context:) type: Types::Bases::Any.new(location: nil), kind: context.attribute_kind, location: nil, - comment: comments[node.first_lineno - 1], + comment: comments[node.start_line - 1], annotations: [] ) end end when :alias_method - if args[0] && args[1] && (new_name = literal_to_symbol(args[0])) && (old_name = literal_to_symbol(args[1])) + if args.size == 2 && (new_name = literal_to_symbol(args[0])) && (old_name = literal_to_symbol(args[1])) decls << AST::Members::Alias.new( new_name: new_name, old_name: old_name, kind: context.singleton ? :singleton : :instance, annotations: [], location: nil, - comment: comments[node.first_lineno - 1], + comment: comments[node.start_line - 1], ) end when :module_function @@ -298,24 +304,26 @@ def process(node, decls:, comments:, context:) else module_func_context = context.update(module_function: true) args.each do |arg| - if arg && (name = literal_to_symbol(arg)) + if (name = literal_to_symbol(arg)) if (i, defn = find_def_index_by_name(decls, name)) if defn.is_a?(AST::Members::MethodDefinition) decls[i] = defn.update(kind: :singleton_instance) end end elsif arg - process arg, decls: decls, comments: comments, context: module_func_context + with(decls: decls, context: module_func_context) do + visit(arg) + end end end end when :public, :private - accessibility = __send__(node.children[0]) + accessibility = __send__(node.name) if args.empty? decls << accessibility else args.each do |arg| - if arg && (name = literal_to_symbol(arg)) + if (name = literal_to_symbol(arg)) if (i, _ = find_def_index_by_name(decls, name)) current = current_accessibility(decls, i) if current != accessibility @@ -329,27 +337,20 @@ def process(node, decls:, comments:, context:) # For `private def foo` syntax current = current_accessibility(decls) decls << accessibility - process_children(node, decls: decls, comments: comments, context: context) + visit(node.arguments) decls << current end else - process_children(node, decls: decls, comments: comments, context: context) + visit(node.arguments) end + end - when :ITER - # ignore - - when :CDECL - const_name = case - when node.children[0].is_a?(Symbol) - TypeName.new(name: node.children[0], namespace: Namespace.empty) - else - const_to_name!(node.children[0], context: context) - end + def visit_constant_write_node(node) + const_name = const_to_name!(node, context: context) - value_node = node.children.last - type = if value_node.nil? || value_node.type == :SELF - # Give up type prediction when node is MASGN or SELF. + value_node = node.value + type = if value_node.is_a?(Prism::SelfNode) + # Give up type prediction. Types::Bases::Any.new(location: nil) else literal_to_type(value_node) @@ -358,25 +359,37 @@ def process(node, decls:, comments:, context:) name: const_name, type: type, location: nil, - comment: comments[node.first_lineno - 1], + comment: comments[node.start_line - 1], annotations: [] ) + end + alias visit_constant_path_write_node visit_constant_write_node - when :IASGN + def visit_constant_target_node(node) + decls << AST::Declarations::Constant.new( + name: TypeName.new(name: node.name, namespace: Namespace.empty), + type: Types::Bases::Any.new(location: nil), + location: nil, + comment: comments[node.start_line - 1], + annotations: [] + ) + end + + def visit_instance_variable_write_node(node) case [context.singleton, context.in_def] when [true, true], [false, false] member = AST::Members::ClassInstanceVariable.new( - name: node.children.first, + name: node.name, type: Types::Bases::Any.new(location: nil), location: nil, - comment: comments[node.first_lineno - 1] + comment: comments[node.start_line - 1] ) when [false, true] member = AST::Members::InstanceVariable.new( - name: node.children.first, + name: node.name, type: Types::Bases::Any.new(location: nil), location: nil, - comment: comments[node.first_lineno - 1] + comment: comments[node.start_line - 1] ) when [true, false] # The variable is for the singleton class of the class object. @@ -386,431 +399,480 @@ def process(node, decls:, comments:, context:) end decls.push member if member && !decls.include?(member) + end + alias visit_instance_variable_or_write_node visit_instance_variable_write_node + alias visit_instance_variable_and_write_node visit_instance_variable_write_node + alias visit_instance_variable_operator_write_node visit_instance_variable_write_node + alias visit_instance_variable_target_node visit_instance_variable_write_node - when :CVASGN + def visit_class_variable_write_node(node) member = AST::Members::ClassVariable.new( - name: node.children.first, + name: node.name, type: Types::Bases::Any.new(location: nil), location: nil, - comment: comments[node.first_lineno - 1] + comment: comments[node.start_line - 1] ) decls.push member unless decls.include?(member) - else - process_children(node, decls: decls, comments: comments, context: context) end - end - - def process_children(node, decls:, comments:, context:) - each_child node do |child| - process child, decls: decls, comments: comments, context: context + alias visit_class_variable_or_write_node visit_class_variable_write_node + alias visit_class_variable_and_write_node visit_class_variable_write_node + alias visit_class_variable_operator_write_node visit_class_variable_write_node + alias visit_class_variable_target_node visit_class_variable_write_node + + def with(decls:, context:) + orig_decls, orig_context = @decls, @context + @decls, @context = decls, context + yield + ensure + @decls, @context = orig_decls, orig_context end - end - def const_to_name!(node, context: nil) - case node.type - when :CONST - TypeName.new(name: node.children[0], namespace: Namespace.empty) - when :COLON2 - if node.children[0] - namespace = const_to_name!(node.children[0], context: context).to_namespace - else - namespace = Namespace.empty - end - - TypeName.new(name: node.children[1], namespace: namespace) - when :COLON3 - TypeName.new(name: node.children[0], namespace: Namespace.root) - when :SELF - raise if context.nil? + def untyped + @untyped ||= Types::Bases::Any.new(location: nil) + end - context.namespace.to_type_name - else - raise + def private + @private ||= AST::Members::Private.new(location: nil) end - end - def const_to_name(node, context:) - if node - case node.type - when :SELF - context.namespace.to_type_name - when :CONST, :COLON2, :COLON3 - const_to_name!(node) rescue nil - end + def public + @public ||= AST::Members::Public.new(location: nil) end - end - def literal_to_symbol(node) - case node.type - when :SYM - node.children[0] - when :LIT - node.children[0] if node.children[0].is_a?(Symbol) - when :STR - node.children[0].to_sym + def current_accessibility(decls, index = decls.size) + slice = decls.slice(0, index) or raise + idx = slice.rindex { |decl| decl == private || decl == public } + if idx + _ = decls[idx] + else + public + end end - end - def function_type_from_body(node, def_name) - table_node, args_node, *_ = node.children + def remove_unnecessary_accessibility_methods!(decls) + # @type var current: decl + current = public + idx = 0 - pre_num, _pre_init, opt, _first_post, post_num, _post_init, rest, kw, kwrest, _block = args_from_node(args_node) + loop do + decl = decls[idx] or break + if current == decl + decls.delete_at(idx) + next + end - return_type = if def_name == :initialize - Types::Bases::Void.new(location: nil) - else - function_return_type_from_body(node) - end + if 0 < idx && is_accessibility?(decls[idx - 1]) && is_accessibility?(decl) + decls.delete_at(idx - 1) + idx -= 1 + current = current_accessibility(decls, idx) + next + end - fun = Types::Function.empty(return_type) + current = decl if is_accessibility?(decl) + idx += 1 + end - table_node.take(pre_num).each do |name| - fun.required_positionals << Types::Function::Param.new(name: name, type: untyped) + decls.pop while decls.last && is_accessibility?(decls.last || raise) end - while opt&.type == :OPT_ARG - lvasgn, opt = opt.children - name = lvasgn.children[0] - fun.optional_positionals << Types::Function::Param.new( - name: name, - type: param_type(lvasgn.children[1]) - ) + def is_accessibility?(decl) + decl == public || decl == private end - if rest - rest_name = rest == :* ? nil : rest # `def f(...)` syntax has `*` name - fun = fun.update(rest_positionals: Types::Function::Param.new(name: rest_name, type: untyped)) + def find_def_index_by_name(decls, name) + index = decls.find_index do |decl| + case decl + when AST::Members::MethodDefinition, AST::Members::AttrReader + decl.name == name + when AST::Members::AttrWriter + :"#{decl.name}=" == name + end + end + + if index + [ + index, + _ = decls[index] + ] + end end - table_node.drop(fun.required_positionals.size + fun.optional_positionals.size + (fun.rest_positionals ? 1 : 0)).take(post_num).each do |name| - fun.trailing_positionals << Types::Function::Param.new(name: name, type: untyped) + def sort_members!(decls) + i = 0 + orders = { + AST::Members::ClassVariable => -3, + AST::Members::ClassInstanceVariable => -2, + AST::Members::InstanceVariable => -1, + } #: Hash[Class, Integer] + decls.sort_by! { |decl| [orders.fetch(decl.class, 0), i += 1] } end - while kw - lvasgn, kw = kw.children - name, value = lvasgn.children + def const_to_name!(node, context: nil) + case node + when Prism::ConstantReadNode, Prism::ConstantWriteNode + TypeName.new(name: node.name, namespace: Namespace.empty) + when Prism::ConstantPathWriteNode + const_to_name!(node.target, context: context) + when Prism::ConstantPathNode + if node.parent + namespace = const_to_name!(node.parent, context: context).to_namespace + else + namespace = Namespace.root + end + + TypeName.new(name: node.name || raise, namespace: namespace) + when Prism::SelfNode + raise if context.nil? - case value - when nil, :NODE_SPECIAL_REQUIRED_KEYWORD - fun.required_keywords[name] = Types::Function::Param.new(name: nil, type: untyped) - when RubyVM::AbstractSyntaxTree::Node - fun.optional_keywords[name] = Types::Function::Param.new(name: nil, type: param_type(value)) + context.namespace.to_type_name else - raise "Unexpected keyword arg value: #{value}" + raise node.class.to_s end end - if kwrest && kwrest.children.any? - kwrest_name = kwrest.children[0] #: Symbol? - kwrest_name = nil if kwrest_name == :** # `def f(...)` syntax has `**` name - fun = fun.update(rest_keywords: Types::Function::Param.new(name: kwrest_name, type: untyped)) + def const_to_name(node, context:) + case node + when Prism::SelfNode + context.namespace.to_type_name + when Prism::ConstantReadNode, Prism::ConstantPathNode + const_to_name!(node) rescue nil + end end - fun - end + def function_type_from_def_node(node) + return_type = if node.name == :initialize + Types::Bases::Void.new(location: nil) + else + body_type(node.body) + end - def function_return_type_from_body(node) - body = node.children[2] - body_type(body) - end + fun = Types::Function.empty(return_type) + + node.parameters&.requireds&.each do |arg| + next unless arg.is_a?(Prism::RequiredParameterNode) + fun.required_positionals << Types::Function::Param.new(name: arg.name, type: untyped) + end + + node.parameters&.optionals&.each do |arg| + fun.optional_positionals << Types::Function::Param.new( + name: arg.name, + type: param_type(arg.value) + ) + end + + if (rest = node.parameters&.rest) + rest_name = rest.is_a?(Prism::RestParameterNode) ? rest.name : nil + fun = fun.update(rest_positionals: Types::Function::Param.new(name: rest_name, type: untyped)) + end - def body_type(node) - return Types::Bases::Nil.new(location: nil) unless node + node.parameters&.posts&.each do |post| + next unless post.is_a?(Prism::RequiredParameterNode) + fun.trailing_positionals << Types::Function::Param.new(name: post.name, type: untyped) + end - case node.type - when :IF, :UNLESS - if_unless_type(node) - when :BLOCK - block_type(node) - else - literal_to_type(node) + node.parameters&.keywords&.each do |kw| + if kw.is_a?(Prism::RequiredKeywordParameterNode) + fun.required_keywords[kw.name] = Types::Function::Param.new(name: nil, type: untyped) + else + fun.optional_keywords[kw.name] = Types::Function::Param.new(name: nil, type: param_type(kw.value)) + end + end + + if (kw_rest = node.parameters&.keyword_rest) + case kw_rest + when Prism::KeywordRestParameterNode + fun = fun.update(rest_keywords: Types::Function::Param.new(name: kw_rest.name, type: untyped)) + when Prism::ForwardingParameterNode + fun = fun.update(rest_positionals: Types::Function::Param.new(name: nil, type: untyped)) + fun = fun.update(rest_keywords: Types::Function::Param.new(name: nil, type: untyped)) + end + end + + fun end - end - def if_unless_type(node) - raise unless node.type == :IF || node.type == :UNLESS + def block_from_body(body_node, parameters) + # @type var body_node: node? + if body_node + yields = any_node?(body_node) {|n| n.is_a?(Prism::YieldNode) } + end + triple_dot = parameters&.keyword_rest.is_a?(Prism::ForwardingParameterNode) - _exp_node, true_node, false_node = node.children - types_to_union_type([body_type(true_node), body_type(false_node)]) - end + if yields || parameters&.block || triple_dot + block_var = parameters&.block&.name + required = !triple_dot - def block_type(node) - raise unless node.type == :BLOCK - - return_stmts = any_node?(node) do |n| - n.type == :RETURN - end&.map do |return_node| - returned_value = return_node.children[0] - returned_value ? literal_to_type(returned_value) : Types::Bases::Nil.new(location: nil) - end || [] - last_node = node.children.last - last_evaluated = last_node ? literal_to_type(last_node) : Types::Bases::Nil.new(location: nil) - types_to_union_type([*return_stmts, last_evaluated]) - end + if body_node + if any_node?(body_node) {|n| n.is_a?(Prism::CallNode) && n.name == :block_given? && !n.receiver && !n.arguments } + required = false + end + end - def literal_to_type(node) - case node.type - when :STR - lit = node.children[0] - if lit.ascii_only? - Types::Literal.new(literal: lit, location: nil) - else - BuiltinNames::String.instance_type + if block_var && body_node + usage = NodeUsage.new(body_node) + if usage.each_conditional_node.any? {|n| n.is_a?(Prism::LocalVariableReadNode) && n.name == block_var } + required = false + end + end + + if yields + function = Types::Function.empty(untyped) + + yields.each do |yield_node| + yield_args = yield_node.arguments&.arguments || [] + + # @type var keywords: node? + positionals, keywords = if keyword_hash?(yield_args.last) + [yield_args.take(yield_args.size - 1), yield_args.last] + else + [yield_args, nil] + end + + if (diff = positionals.size - function.required_positionals.size) > 0 + diff.times do + function.required_positionals << Types::Function::Param.new( + type: untyped, + name: nil + ) + end + end + + if keywords + keywords.elements.each do |assoc_node| + key = assoc_node.key.value.to_sym + function.required_keywords[key] ||= + Types::Function::Param.new( + type: untyped, + name: nil + ) + end + end + end + else + function = Types::UntypedFunction.new(return_type: untyped) + end + + Types::Block.new(required: required, type: function, self_type: nil) end - when :DSTR, :XSTR - BuiltinNames::String.instance_type - when :SYM - lit = node.children[0] - if lit.to_s.ascii_only? - Types::Literal.new(literal: lit, location: nil) + end + + def body_type(node) + return Types::Bases::Nil.new(location: nil) unless node + + node = node.body.first if node.is_a?(Prism::StatementsNode) && node.body.size == 1 + case node + when Prism::IfNode + types_to_union_type([body_type(node.statements), body_type(node.subsequent)]) + when Prism::UnlessNode + types_to_union_type([body_type(node.statements), body_type(node.else_clause)]) + when Prism::ElseNode + body_type(node.statements) + when Prism::StatementsNode + block_type(node) else - BuiltinNames::Symbol.instance_type + literal_to_type(node) end - when :DSYM - BuiltinNames::Symbol.instance_type - when :DREGX, :REGX - BuiltinNames::Regexp.instance_type - when :TRUE - Types::Literal.new(literal: true, location: nil) - when :FALSE - Types::Literal.new(literal: false, location: nil) - when :NIL - Types::Bases::Nil.new(location: nil) - when :INTEGER - Types::Literal.new(literal: node.children[0], location: nil) - when :FLOAT - BuiltinNames::Float.instance_type - when :RATIONAL, :IMAGINARY - lit = node.children[0] - type_name = TypeName.new(name: lit.class.name.to_sym, namespace: Namespace.root) - Types::ClassInstance.new(name: type_name, args: [], location: nil) - when :LIT - lit = node.children[0] - case lit - when Symbol + end + + def block_type(node) + return_stmts = any_node?(node) do |n| + n.is_a?(Prism::ReturnNode) + end&.map do |return_node| + return_args = return_node.arguments&.arguments || [] + return_types = return_args.map { |arg| literal_to_type(arg) } + + if return_types.size >= 2 + t = types_to_union_type(return_types) + BuiltinNames::Array.instance_type(t) + else + return_types.first || Types::Bases::Nil.new(location: nil) + end + end || [] + + last_node = node.compact_child_nodes.last + last_evaluated = last_node ? literal_to_type(last_node) : Types::Bases::Nil.new(location: nil) + # FIXME: Skipt if last_evaluated ReturnNode + types_to_union_type([*return_stmts, last_evaluated]) + end + + def literal_to_symbol(node) + case node + when Prism::SymbolNode, Prism::StringNode + node.unescaped.to_sym + end + end + + def literal_to_type(node) + case node + in Prism::StringNode + lit = node.unescaped + if lit.ascii_only? + Types::Literal.new(literal: lit, location: nil) + else + BuiltinNames::String.instance_type + end + in Prism::InterpolatedStringNode | Prism::XStringNode | Prism::InterpolatedXStringNode + BuiltinNames::String.instance_type + in Prism::SymbolNode + lit = node.unescaped.to_sym if lit.to_s.ascii_only? Types::Literal.new(literal: lit, location: nil) else BuiltinNames::Symbol.instance_type end - when Integer - Types::Literal.new(literal: lit, location: nil) - when String - # For Ruby <=3.3 which generates `LIT` node for string literals inside Hash literal. - # "a" => STR node - # { "a" => nil } => LIT node - Types::Literal.new(literal: lit, location: nil) - else + in Prism::InterpolatedSymbolNode + BuiltinNames::Symbol.instance_type + in Prism::RegularExpressionNode | Prism::InterpolatedRegularExpressionNode + BuiltinNames::Regexp.instance_type + in Prism::TrueNode + Types::Literal.new(literal: true, location: nil) + in Prism::FalseNode + Types::Literal.new(literal: false, location: nil) + in Prism::NilNode + Types::Bases::Nil.new(location: nil) + in Prism::IntegerNode + Types::Literal.new(literal: node.value, location: nil) + in Prism::FloatNode + BuiltinNames::Float.instance_type + in Prism::RationalNode | Prism::ImaginaryNode + lit = node.value type_name = TypeName.new(name: lit.class.name.to_sym, namespace: Namespace.root) Types::ClassInstance.new(name: type_name, args: [], location: nil) - end - when :ZLIST, :ZARRAY - BuiltinNames::Array.instance_type(untyped) - when :LIST, :ARRAY - elem_types = node.children.compact.map { |e| literal_to_type(e) } - t = types_to_union_type(elem_types) - BuiltinNames::Array.instance_type(t) - when :DOT2, :DOT3 - types = node.children.map { |c| literal_to_type(c) } - type = range_element_type(types) - BuiltinNames::Range.instance_type(type) - when :HASH - list = node.children[0] - if list - children = list.children - children.pop - else - children = [] #: Array[untyped] - end + in Prism::ArrayNode + elem_types = node.compact_child_nodes.map { |e| literal_to_type(e) } + t = types_to_union_type(elem_types) + BuiltinNames::Array.instance_type(t) + in Prism::RangeNode + types = [literal_to_type(node.left), literal_to_type(node.right)] + type = range_element_type(types) + BuiltinNames::Range.instance_type(type) + in Prism::HashNode | Prism::KeywordHashNode + key_types = [] #: Array[Types::t] + value_types = [] #: Array[Types::t] + node.elements.each do |element| + if element.is_a?(Prism::AssocNode) + key_types << literal_to_type(element.key) + value_types << literal_to_type(element.value) + else + key_types << untyped + value_types << untyped + end + end - key_types = [] #: Array[Types::t] - value_types = [] #: Array[Types::t] - children.each_slice(2) do |k, v| - if k - key_types << literal_to_type(k) - value_types << literal_to_type(v) + if !key_types.empty? && key_types.all? { |t| t.is_a?(Types::Literal) } + fields = key_types.map {|t| + t.is_a?(Types::Literal) or raise + t.literal + }.zip(value_types).to_h #: Hash[Types::Literal::literal, Types::t] + Types::Record.new(fields: fields, location: nil) else - key_types << untyped - value_types << untyped + key_type = types_to_union_type(key_types) + value_type = types_to_union_type(value_types) + BuiltinNames::Hash.instance_type(key_type, value_type) + end + in Prism::SelfNode + Types::Bases::Self.new(location: nil) + in Prism::CallNode[receiver: receiver] + case node.name + when :freeze, :tap, :itself, :dup, :clone, :taint, :untaint, :extend + literal_to_type(receiver) + else + untyped end - end - - if !key_types.empty? && key_types.all? { |t| t.is_a?(Types::Literal) } - fields = key_types.map {|t| - t.is_a?(Types::Literal) or raise - t.literal - }.zip(value_types).to_h #: Hash[Types::Literal::literal, Types::t] - Types::Record.new(fields: fields, location: nil) - else - key_type = types_to_union_type(key_types) - value_type = types_to_union_type(value_types) - BuiltinNames::Hash.instance_type(key_type, value_type) - end - when :SELF - Types::Bases::Self.new(location: nil) - when :CALL - receiver, method_name, * = node.children - case method_name - when :freeze, :tap, :itself, :dup, :clone, :taint, :untaint, :extend - literal_to_type(receiver) else untyped end - else - untyped end - end - def types_to_union_type(types) - return untyped if types.empty? + def types_to_union_type(types) + return untyped if types.empty? - uniq = types.uniq - if uniq.size == 1 - return uniq.first || raise + uniq = types.uniq + if uniq.size == 1 + return uniq.first || raise + end + + Types::Union.new(types: uniq, location: nil) end - Types::Union.new(types: uniq, location: nil) - end + def range_element_type(types) + types = types.reject { |t| t == untyped } + return untyped if types.empty? - def range_element_type(types) - types = types.reject { |t| t == untyped } - return untyped if types.empty? + types = types.map do |t| + if t.is_a?(Types::Literal) + type_name = TypeName.new(name: t.literal.class.name&.to_sym || raise, namespace: Namespace.root) + Types::ClassInstance.new(name: type_name, args: [], location: nil) + else + t + end + end.uniq - types = types.map do |t| - if t.is_a?(Types::Literal) - type_name = TypeName.new(name: t.literal.class.name&.to_sym || raise, namespace: Namespace.root) - Types::ClassInstance.new(name: type_name, args: [], location: nil) + if types.size == 1 + types.first or raise else - t + untyped end - end.uniq - - if types.size == 1 - types.first or raise - else - untyped end - end - def param_type(node, default: Types::Bases::Any.new(location: nil)) - case node.type - when :INTEGER - BuiltinNames::Integer.instance_type - when :FLOAT - BuiltinNames::Float.instance_type - when :RATIONAL - Types::ClassInstance.new(name: TypeName.parse("::Rational"), args: [], location: nil) - when :IMAGINARY - Types::ClassInstance.new(name: TypeName.parse("::Complex"), args: [], location: nil) - when :LIT - case node.children[0] - when Symbol - BuiltinNames::Symbol.instance_type - when Integer + def param_type(node, default: Types::Bases::Any.new(location: nil)) + case node + when Prism::IntegerNode BuiltinNames::Integer.instance_type - when Float + when Prism::FloatNode BuiltinNames::Float.instance_type + when Prism::RationalNode + Types::ClassInstance.new(name: TypeName.parse("::Rational"), args: [], location: nil) + when Prism::ImaginaryNode + Types::ClassInstance.new(name: TypeName.parse("::Complex"), args: [], location: nil) + when Prism::SymbolNode, Prism::InterpolatedSymbolNode + BuiltinNames::Symbol.instance_type + when Prism::StringNode, Prism::InterpolatedStringNode, Prism::XStringNode, Prism::InterpolatedStringNode + BuiltinNames::String.instance_type + when Prism::NilNode + # This type is technical non-sense, but may help practically. + Types::Optional.new( + type: Types::Bases::Any.new(location: nil), + location: nil + ) + when Prism::TrueNode, Prism::FalseNode + Types::Bases::Bool.new(location: nil) + when Prism::ArrayNode + # FIXME bug replicating empty array untyped + if node.elements.any? + BuiltinNames::Array.instance_type(default) + else + default + end + when Prism::HashNode + BuiltinNames::Hash.instance_type(default, default) else default end - when :SYM - BuiltinNames::Symbol.instance_type - when :STR, :DSTR - BuiltinNames::String.instance_type - when :NIL - # This type is technical non-sense, but may help practically. - Types::Optional.new( - type: Types::Bases::Any.new(location: nil), - location: nil - ) - when :TRUE, :FALSE - Types::Bases::Bool.new(location: nil) - when :ARRAY, :LIST - BuiltinNames::Array.instance_type(default) - when :HASH - BuiltinNames::Hash.instance_type(default, default) - else - default - end - end - - # backward compatible - alias node_type param_type - - def private - @private ||= AST::Members::Private.new(location: nil) - end - - def public - @public ||= AST::Members::Public.new(location: nil) - end - - def current_accessibility(decls, index = decls.size) - slice = decls.slice(0, index) or raise - idx = slice.rindex { |decl| decl == private || decl == public } - if idx - _ = decls[idx] - else - public end - end - def remove_unnecessary_accessibility_methods!(decls) - # @type var current: decl - current = public - idx = 0 + # backward compatible + alias node_type param_type - loop do - decl = decls[idx] or break - if current == decl - decls.delete_at(idx) - next + def any_node?(node, nodes: [], &block) + if yield(node) + nodes << node end - if 0 < idx && is_accessibility?(decls[idx - 1]) && is_accessibility?(decl) - decls.delete_at(idx - 1) - idx -= 1 - current = current_accessibility(decls, idx) - next + node.compact_child_nodes.each do |child| + any_node? child, nodes: nodes, &block end - current = decl if is_accessibility?(decl) - idx += 1 + nodes.empty? ? nil : nodes end - decls.pop while decls.last && is_accessibility?(decls.last || raise) - end + def keyword_hash?(node) + return false unless node.is_a?(Prism::KeywordHashNode) - def is_accessibility?(decl) - decl == public || decl == private - end - - def find_def_index_by_name(decls, name) - index = decls.find_index do |decl| - case decl - when AST::Members::MethodDefinition, AST::Members::AttrReader - decl.name == name - when AST::Members::AttrWriter - :"#{decl.name}=" == name + node.elements.all? do |element| + element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode) end end - - if index - [ - index, - _ = decls[index] - ] - end - end - - def sort_members!(decls) - i = 0 - orders = { - AST::Members::ClassVariable => -3, - AST::Members::ClassInstanceVariable => -2, - AST::Members::InstanceVariable => -1, - } #: Hash[Class, Integer] - decls.sort_by! { |decl| [orders.fetch(decl.class, 0), i += 1] } end end end diff --git a/lib/rbs/prototype/runtime.rb b/lib/rbs/prototype/runtime.rb index 6cf73b879b..9ee2fa75de 100644 --- a/lib/rbs/prototype/runtime.rb +++ b/lib/rbs/prototype/runtime.rb @@ -675,6 +675,116 @@ def block_from_ast_of(method) block_from_body(ast) end end + + def block_from_body(node) + _, args_node, body_node = node.children + _pre_num, _pre_init, _opt, _first_post, _post_num, _post_init, _rest, _kw, _kwrest, block_var = args_from_node(args_node) + + # @type var body_node: node? + if body_node + yields = any_node?(body_node) {|n| n.type == :YIELD } + end + + if yields || block_var + required = true + + if body_node + if any_node?(body_node) {|n| n.type == :FCALL && n.children[0] == :block_given? && !n.children[1] } + required = false + end + end + + if _rest == :* && block_var == :& + # ... is given + required = false + end + + if block_var + if body_node + usage = NodeUsage.new(body_node) + if usage.each_conditional_node.any? {|n| n.type == :LVAR && n.children[0] == block_var } + required = false + end + end + end + + if yields + function = Types::Function.empty(untyped) + + yields.each do |yield_node| + array_content = yield_node.children[0]&.children&.compact || [] + + # @type var keywords: node? + positionals, keywords = if keyword_hash?(array_content.last) + [array_content.take(array_content.size - 1), array_content.last] + else + [array_content, nil] + end + + if (diff = positionals.size - function.required_positionals.size) > 0 + diff.times do + function.required_positionals << Types::Function::Param.new( + type: untyped, + name: nil + ) + end + end + + if keywords + keywords.children[0].children.each_slice(2) do |key_node, value_node| + if key_node + key = key_node.children[0] + function.required_keywords[key] ||= + Types::Function::Param.new( + type: untyped, + name: nil + ) + end + end + end + end + else + function = Types::UntypedFunction.new(return_type: untyped) + end + + + Types::Block.new(required: required, type: function, self_type: nil) + end + end + + # NOTE: args_node may be a nil by a bug + # https://bugs.ruby-lang.org/issues/17495 + def args_from_node(args_node) + args_node&.children || [0, nil, nil, nil, 0, nil, nil, nil, nil, nil] + end + + def any_node?(node, nodes: [], &block) + if yield(node) + nodes << node + end + + node.children.each do |child| + if child.is_a?(RubyVM::AbstractSyntaxTree::Node) + any_node?(child, nodes: nodes, &block) + end + end + + nodes.empty? ? nil : nodes + end + + def keyword_hash?(node) + if node && node.type == :HASH + node.children[0].children.compact.each_slice(2).all? {|key, _| + symbol_literal_node?(key) + } + else + false + end + end + + def untyped + @untyped ||= Types::Bases::Any.new(location: nil) + end end end end diff --git a/sig/cli.rbs b/sig/cli.rbs index 5b56646ac7..075c59017e 100644 --- a/sig/cli.rbs +++ b/sig/cli.rbs @@ -44,8 +44,6 @@ module RBS def parse_logging_options: (OptionParser) -> void - def has_parser?: (String format) -> bool - def run: (Array[String] args) -> Integer def run_ast: (Array[String], LibraryOptions) -> Integer diff --git a/sig/prototype/helpers.rbs b/sig/prototype/helpers.rbs index 634c712136..b14989236d 100644 --- a/sig/prototype/helpers.rbs +++ b/sig/prototype/helpers.rbs @@ -1,31 +1,7 @@ module RBS module Prototype module Helpers - type node = RubyVM::AbstractSyntaxTree::Node - - def parse_comments: (String, include_trailing: bool) -> Hash[Integer, AST::Comment] - def process_comments: (Array[Prism::Comment] comments, include_trailing: bool) -> Hash[Integer, AST::Comment] - - def block_from_body: (node) -> Types::Block? - - def each_node: (Array[untyped] nodes) { (node) -> void } -> void - - def each_child: (node) { (node child) -> void } -> void - - def any_node?: (node, ?nodes: Array[node]) { (node) -> bool } -> Array[node]? - - def keyword_hash?: (node) -> bool - - # Returns a symbol if the node is a symbol literal node - # - def symbol_literal_node?: (node) -> Symbol? - - def args_from_node: (node?) -> Array[untyped] - - def untyped: () -> Types::Bases::Any - - @untyped: Types::Bases::Any end end end diff --git a/sig/prototype/node_usage.rbs b/sig/prototype/node_usage.rbs index 6a72f8346b..535cf108d9 100644 --- a/sig/prototype/node_usage.rbs +++ b/sig/prototype/node_usage.rbs @@ -2,19 +2,17 @@ module RBS module Prototype class NodeUsage include Helpers - - type node = RubyVM::AbstractSyntaxTree::Node - attr_reader node: node + @node: Prism::Node - attr_reader conditional_nodes: Set[node] + attr_reader conditional_nodes: Set[Prism::Node] - def initialize: (node) -> void + def initialize: (Prism::Node) -> void - def calculate: (node, conditional: bool) -> void + def calculate: (Prism::Node, conditional: bool) -> void - def each_conditional_node: () { (node) -> void } -> void - | () -> Enumerator[node, void] + def each_conditional_node: () { (Prism::Node) -> void } -> void + | () -> Enumerator[Prism::Node, void] end end end diff --git a/sig/prototype/rb.rbs b/sig/prototype/rb.rbs index 7b5bda5e55..7479fd0793 100644 --- a/sig/prototype/rb.rbs +++ b/sig/prototype/rb.rbs @@ -1,7 +1,7 @@ module RBS module Prototype class RB - include Helpers + extend Helpers class Context type method_kind = :singleton | :singleton_instance | :instance @@ -29,68 +29,76 @@ module RBS type decl = AST::Declarations::t | AST::Members::t - attr_reader source_decls: Array[decl] + def self.parse: (String) -> Array[AST::Declarations::t] - def initialize: () -> void + def self.process_decls: (Array[AST::Declarations::t | AST::Members::t]) -> Array[AST::Declarations::t] - def decls: () -> Array[AST::Declarations::t] + class Visitor < Prism::Visitor + attr_reader context: Context - def parse: (String) -> Array[AST::Declarations::t] + attr_reader comments: Hash[Integer, AST::Comment] - def process: (untyped node, decls: Array[AST::Declarations::t | AST::Members::t], comments: Hash[Integer, AST::Comment], context: Context) -> void + attr_reader decls: Array[decl] - def process_children: (RubyVM::AbstractSyntaxTree::Node node, decls: Array[decl], comments: Hash[Integer, AST::Comment], context: Context) -> void + def initialize: (Hash[Integer, AST::Comment] comments) -> void - # Returns a type name that represents the name of the constant. - # `node` must be _constant_ node, `CONST`, `COLON2`, or `COLON3` node. - # - def const_to_name!: (RubyVM::AbstractSyntaxTree::Node node, ?context: Context?) -> TypeName + def with: (decls: Array[decl], context: Context) { () -> untyped } -> void - # Returns a type name that represents the name of the constant. - # `node` can be `SELF` for `extend self` pattern. - # - def const_to_name: (RubyVM::AbstractSyntaxTree::Node? node, context: Context) -> TypeName? + # Returns a type name that represents the name of the constant. + def const_to_name!: (Prism::Node node, ?context: Context?) -> TypeName - def literal_to_symbol: (RubyVM::AbstractSyntaxTree::Node node) -> Symbol? + # Returns a type name that represents the name of the constant. + # `node` can be `SELF` for `extend self` pattern. + # + def const_to_name: (Prism::Node? node, context: Context) -> TypeName? - def function_type_from_body: (RubyVM::AbstractSyntaxTree::Node node, Symbol def_name) -> Types::Function + def literal_to_symbol: (Prism::Node node) -> Symbol? - def function_return_type_from_body: (RubyVM::AbstractSyntaxTree::Node node) -> Types::t + def function_type_from_def_node: (Prism::DefNode node) -> Types::Function - def body_type: (RubyVM::AbstractSyntaxTree::Node node) -> Types::t + def block_from_body: (Prism::StatementsNode | Prism::BeginNode | nil, Prism::ParametersNode | nil) -> Types::Block - def if_unless_type: (RubyVM::AbstractSyntaxTree::Node node) -> Types::t + def body_type: (Prism::Node? node) -> Types::t - def block_type: (RubyVM::AbstractSyntaxTree::Node node) -> Types::t + def block_type: (Prism::Node node) -> Types::t - def literal_to_type: (RubyVM::AbstractSyntaxTree::Node node) -> Types::t + def literal_to_type: (Prism::Node node) -> Types::t - def types_to_union_type: (Array[Types::t] types) -> Types::t + def types_to_union_type: (Array[Types::t] types) -> Types::t - def range_element_type: (Array[Types::t] types) -> Types::t + def range_element_type: (Array[Types::t] types) -> Types::t - def param_type: (RubyVM::AbstractSyntaxTree::Node node, ?default: Types::Bases::Any) -> Types::t + def param_type: (Prism::Node node, ?default: Types::Bases::Any) -> Types::t - # backward compatible - alias node_type param_type + def any_node?: (Prism::Node node, ?nodes: Array[Prism::Node]) { (Prism::Node) -> bool } -> untyped - def private: () -> AST::Members::Private + def keyword_hash?: (Prism::Node) -> bool - @private: AST::Members::Private? + # backward compatible + alias node_type param_type - def public: () -> AST::Members::Public + def private: () -> AST::Members::Private - @public: AST::Members::Public? + @private: AST::Members::Private? - def current_accessibility: (Array[decl] decls, ?Integer index) -> (AST::Members::Private | AST::Members::Public) + def public: () -> AST::Members::Public - def remove_unnecessary_accessibility_methods!: (Array[decl]) -> void + @public: AST::Members::Public? - def is_accessibility?: (decl) -> bool + def untyped: () -> Types::Bases::Any - def find_def_index_by_name: (Array[decl] decls, Symbol name) -> [Integer, AST::Members::MethodDefinition | AST::Members::AttrReader | AST::Members::AttrWriter]? + @untyped: Types::Bases::Any? - def sort_members!: (Array[decl] decls) -> void + def current_accessibility: (Array[decl] decls, ?Integer index) -> (AST::Members::Private | AST::Members::Public) + + def remove_unnecessary_accessibility_methods!: (Array[decl]) -> void + + def is_accessibility?: (decl) -> bool + + def find_def_index_by_name: (Array[decl] decls, Symbol name) -> [Integer, AST::Members::MethodDefinition | AST::Members::AttrReader | AST::Members::AttrWriter]? + + def sort_members!: (Array[decl] decls) -> void + end end end end diff --git a/sig/prototype/runtime.rbs b/sig/prototype/runtime.rbs index 7d9464dfb1..57d9fb6ece 100644 --- a/sig/prototype/runtime.rbs +++ b/sig/prototype/runtime.rbs @@ -175,6 +175,16 @@ module RBS def can_alias?: (Module, UnboundMethod) -> bool def type_params: (Module) -> Array[AST::TypeParam] + + def args_from_node: (RubyVM::AbstractSyntaxTree::Node?) -> Array[untyped] + + def any_node?: (RubyVM::AbstractSyntaxTree::Node, ?nodes: Array[RubyVM::AbstractSyntaxTree::Node]) { (RubyVM::AbstractSyntaxTree::Node) -> bool } -> Array[RubyVM::AbstractSyntaxTree::Node]? + + def keyword_hash?: (RubyVM::AbstractSyntaxTree::Node) -> bool + + # Returns a symbol if the node is a symbol literal node + # + def symbol_literal_node?: (RubyVM::AbstractSyntaxTree::Node) -> Symbol? end end end diff --git a/test/rbs/cli_test.rb b/test/rbs/cli_test.rb index d8407d6be7..958494eb00 100644 --- a/test/rbs/cli_test.rb +++ b/test/rbs/cli_test.rb @@ -896,29 +896,7 @@ def test_parse_method_type end end - def test_prototype_no_parser - omit_on_truffle_ruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on TruffleRuby" - omit_on_jruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on JRuby" - - Dir.mktmpdir do |dir| - with_cli do |cli| - def cli.has_parser?(format) - false - end - - refute_cli_success cli.run(%w(prototype rb)) - refute_cli_success cli.run(%w(prototype rbi)) - - assert_equal "Not supported on this interpreter (ruby).\n", stdout.string.lines[0] - assert_equal "Not supported on this interpreter (ruby).\n", stdout.string.lines[1] - end - end - end - def test_prototype_batch - omit_on_truffle_ruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on TruffleRuby" - omit_on_jruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on JRuby" - Dir.mktmpdir do |dir| dir = Pathname(dir) @@ -980,9 +958,6 @@ module C end def test_prototype_batch_outer - omit_on_truffle_ruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on TruffleRuby" - omit_on_jruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on JRuby" - Dir.mktmpdir do |dir| dir = Pathname(dir) @@ -1009,9 +984,6 @@ module A end def test_prototype_batch_syntax_error - omit_on_truffle_ruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on TruffleRuby" - omit_on_jruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on JRuby" - Dir.mktmpdir do |dir| dir = Pathname(dir) @@ -1060,8 +1032,8 @@ def test_prototype__runtime__todo def test_test - omit_on_truffle_ruby! "`rbs test` relies on `TracePoint` `:end` event, which is not supported on TruffleRuby" - omit_on_jruby! "`rbs test` relies on `TracePoint` `:end` event, which is not supported on JRuby" + omit_on_jruby! "Errno::ENOENT: No such file or directory - exit" + omit_on_truffle_ruby! "Errno::ENOENT: No such file or directory - exit" Dir.mktmpdir do |dir| dir = Pathname(dir) diff --git a/test/rbs/node_usage_test.rb b/test/rbs/node_usage_test.rb index 7f20677afc..a10596388e 100644 --- a/test/rbs/node_usage_test.rb +++ b/test/rbs/node_usage_test.rb @@ -4,14 +4,11 @@ class RBS::NodeUsageTest < Test::Unit::TestCase include RBS::Prototype def parse(string) - RubyVM::AbstractSyntaxTree.parse(string) + Prism.parse(string).value end def test_conditional - omit_on_truffle_ruby! "`RubyVM::AbstractSyntaxTree` is not available on TruffleRuby" - omit_on_jruby! "`RubyVM::AbstractSyntaxTree` is not available on JRuby" - - NodeUsage.new(parse(<<~RB)) + usage = NodeUsage.new(parse(<<~RB)) def block yield end @@ -46,5 +43,7 @@ def block (foo(); bar; baz) ] RB + + assert_equal(8, usage.conditional_nodes.size) end end diff --git a/test/rbs/rb_prototype_test.rb b/test/rbs/rb_prototype_test.rb index afe5d5c790..29260f5f0e 100644 --- a/test/rbs/rb_prototype_test.rb +++ b/test/rbs/rb_prototype_test.rb @@ -1,16 +1,11 @@ require "test_helper" class RBS::RbPrototypeTest < Test::Unit::TestCase - omit_on_truffle_ruby! "`RubyVM::AbstractSyntaxTree` is not available on TruffleRuby" - omit_on_jruby! "`RubyVM::AbstractSyntaxTree` is not available on JRuby" - RB = RBS::Prototype::RB include TestHelper def test_class_module - parser = RB.new - rb = <<-EOR class Hello end @@ -25,7 +20,7 @@ class Bar < Struct.new(:bar) end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello end @@ -41,8 +36,6 @@ class Bar end def test_defs - parser = RB.new - rb = <<-EOR class Hello def hello(a, b = 3, *c, d, e:, f: 3, **g, &h) @@ -59,7 +52,7 @@ def kw_req(a:) end end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello def hello: (untyped a, ?::Integer b, *untyped c, untyped d, e: untyped, ?f: ::Integer, **untyped g) { (?) -> untyped } -> nil @@ -71,8 +64,6 @@ def kw_req: (a: untyped) -> nil end def test_defs_return_type - parser = RB.new - rb = <<-'EOR' class Hello def initialize() 'foo' end @@ -115,7 +106,7 @@ def self1() self end end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello def initialize: () -> void @@ -177,8 +168,6 @@ def self1: () -> self end def test_defs_return_type_with_block - parser = RB.new - rb = <<-'EOR' class Hello def with_return @@ -213,7 +202,7 @@ def when_last_is_nil end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello def with_return: () -> (1 | "2" | :x) @@ -229,8 +218,6 @@ def when_last_is_nil: () -> nil end def test_defs_return_type_with_block_optional - parser = RB.new - rb = <<~'EOR' class Hello def with_optional_block1 @@ -249,7 +236,7 @@ def with_optional_block2(&block) end EOR - assert_write parser.parse(rb), <<~EOF + assert_write RB.parse(rb), <<~EOF class Hello def with_optional_block1: () ?{ (untyped) -> untyped } -> (untyped | nil) @@ -259,8 +246,6 @@ def with_optional_block2: () ?{ (untyped) -> untyped } -> (untyped | nil) end def test_defs_return_type_with_if - parser = RB.new - rb = <<-EOR class ReturnTypeWithIF def with_if @@ -318,7 +303,7 @@ def with_unless end EOR - assert_write parser.parse(rb), <<-EOR + assert_write RB.parse(rb), <<-EOR class ReturnTypeWithIF def with_if: () -> (true | nil) @@ -336,8 +321,6 @@ def with_unless: () -> (:sym | nil) end def test_sclass - parser = RB.new - rb = <<-EOR class Hello class << self @@ -346,7 +329,7 @@ def hello() end end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello def self.hello: () -> nil end @@ -354,8 +337,6 @@ def self.hello: () -> nil end def test_meta_programming - parser = RB.new - rb = <<-EOR class Hello include Foo @@ -384,7 +365,7 @@ module Mod2 end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello include Foo @@ -426,8 +407,6 @@ module Mod2 end def test_module_function - parser = RB.new - rb = <<-EOR module Hello def foo() end @@ -445,7 +424,7 @@ def foobar() end end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF module Hello def foo: () -> nil @@ -459,8 +438,6 @@ def self?.foobar: () -> nil end def test_accessibility - parser = RB.new - rb = <<-EOR class Hello attr_reader :private_attr @@ -487,7 +464,7 @@ def prv4() end end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello private @@ -517,8 +494,6 @@ def prv4: () -> nil end def test_accessibility_and_sclass - parser = RB.new - rb = <<~RUBY class C class << self @@ -531,7 +506,7 @@ def bar() end end RUBY - assert_write parser.parse(rb), <<~RBS + assert_write RB.parse(rb), <<~RBS class C private @@ -545,8 +520,6 @@ def bar: () -> nil end def test_aliases - parser = RB.new - rb = <<-EOR class Hello alias a b @@ -562,7 +535,7 @@ class << self end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello alias a b @@ -576,8 +549,6 @@ class Hello end def test_comments - parser = RB.new - rb = <<-EOR # Comments for class. # This is a comment. @@ -609,7 +580,7 @@ def self.world end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF # Comments for class. # This is a comment. class Hello @@ -641,14 +612,12 @@ def self.world: () -> nil end def test_toplevel - parser = RB.new - rb = <<-EOR def hello end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class Object def hello: () -> nil end @@ -656,8 +625,6 @@ def hello: () -> nil end def test_const - parser = RB.new - rb = <<-EOR module Foo VERSION = '0.1.1' @@ -667,7 +634,7 @@ module Foo end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF module Foo VERSION: "0.1.1" @@ -681,15 +648,13 @@ module Foo end def test_const_with_multi_assign - parser = RB.new - rb = <<-EOR module Foo MAJOR, MINOR, PATCH = ['0', '1', '1'] end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF module Foo MAJOR: untyped @@ -700,9 +665,25 @@ module Foo EOF end - def test_literal_types - parser = RB.new + def test_const_with_multi_assign_ivar + rb = <<~RUBY + module Foo + @major, @minor, @patch = ['0', '1', '1'] + end + RUBY + + assert_write RB.parse(rb), <<~RBS + module Foo + self.@major: untyped + + self.@minor: untyped + + self.@patch: untyped + end + RBS + end + def test_literal_types rb = <<-'EOR' A = 1 B = 1.0 @@ -715,7 +696,7 @@ def test_literal_types I = self EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF A: 1 B: ::Float @@ -737,14 +718,11 @@ def test_literal_types end def test_invalid_byte_sequence_in_utf8 - parser = RB.new rb = 'A = "\xff"' - assert_write parser.parse(rb), "A: ::String\n" + assert_write RB.parse(rb), "A: ::String\n" end - def test_argumentless_fcall - parser = RB.new - + def test_argumentless_call rb = <<-'EOR' class C included do @@ -753,15 +731,13 @@ class C end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class C end EOF end - def test_method_definition_in_fcall - parser = RB.new - + def test_method_definition_in_call rb = <<-'EOR' class C some_method_takes_method_name def foo @@ -769,7 +745,7 @@ class C end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class C def foo: () -> nil end @@ -777,8 +753,6 @@ def foo: () -> nil end def test_multiple_nested_class - parser = RB.new - rb = <<-'EOR' module Foo class Bar @@ -791,7 +765,7 @@ class Baz end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF module Foo class Bar end @@ -805,8 +779,6 @@ class Baz end def test_duplicate_methods - parser = RB.new - rb = <<-'EOR' class C def foo(x, y, z) @@ -815,16 +787,14 @@ def foo(x, y, z) end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class C def foo: (untyped x, untyped y, untyped z) -> untyped end EOF end - def test_ITER - parser = RB.new - + def test_block_content rb = <<~'RUBY' module M def not_refinements @@ -842,7 +812,7 @@ def in_included end RUBY - assert_write parser.parse(rb), <<~RBS + assert_write RB.parse(rb), <<~RBS module M def not_refinements: () -> nil end @@ -850,8 +820,6 @@ def not_refinements: () -> nil end def test_calling_class_method_from_instance - parser = RB.new - rb = <<~'RUBY' class HelloWorld def self.world(str) @@ -864,7 +832,7 @@ def hello end RUBY - assert_write parser.parse(rb), <<~RBS + assert_write RB.parse(rb), <<~RBS class HelloWorld def self.world: (untyped str) -> untyped @@ -874,30 +842,36 @@ def hello: () -> untyped end def test_instance_variables - parser = RB.new - rb = <<-EOR class Hello def message(message) # comment for ivar @message = message + + @a ||= 1 + @b &&= 2 + @c += 3 end end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello # comment for ivar @message: untyped + @a: untyped + + @b: untyped + + @c: untyped + def message: (untyped message) -> untyped end EOF end def test_instance_variables_in_module - parser = RB.new - rb = <<-EOR module Hello def message(message) @@ -911,7 +885,7 @@ def foo end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF module Hello # comment for ivar @message: untyped @@ -925,9 +899,25 @@ def foo: () -> untyped EOF end - def test_class_variables - parser = RB.new + def test_instance_variable_in_receiver + rb = <<~RUBY + class Hello + def options(options = {}) + (@options ||= { key: SecureRandom.hex }).merge!(options) + end + end + RUBY + + assert_write RB.parse(rb), <<~RBS + class Hello + @options: untyped + def options: (?::Hash[untyped, untyped] options) -> untyped + end + RBS + end + + def test_class_variables rb = <<-EOR class Hello def self.foo @@ -951,7 +941,7 @@ def message(message) end EOR - assert_write parser.parse(rb), <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello # comment for cvar @@message: untyped @@ -971,25 +961,52 @@ def message: (untyped message) -> untyped EOF end + def test_class_variables_conditional_assign + rb = <<~RUBY + class Hello + @@foo &&= 1 + @@bar ||= 2 + @@baz += 3 + end + RUBY + + assert_write RB.parse(rb), <<~RBS + class Hello + @@foo: untyped + + @@bar: untyped + + @@baz: untyped + end + RBS + end + def test_literal_to_type - parser = RBS::Prototype::RB.new + visitor = RB::Visitor.new([]) [ [%{"abc"}, %{"abc"}], + [%{"abc\#{d}"}, %{::String}], + [%{`abc`}, %{::String}], + [%{`abc\#{d}`}, %{::String}], [%{:abc}, %{:abc}], + [%{:"abc\#{d}"}, %{::Symbol}], + [%{/abc/}, %{::Regexp}], + [%{/abc\#{d}/}, %{::Regexp}], [%{[]}, %{::Array[untyped]}], [%{[true]}, %{::Array[true]}], + [%{[foo: true]}, %{::Array[{foo: true}]}], [%{1..2}, %{::Range[::Integer]}], [%{{}}, %{::Hash[untyped, untyped]}], [%{{a: nil}}, %{ { a: nil } }], [%({"a" => /b/}), %({ 'a' => ::Regexp })], ].each do |rb, rbs| - node = RubyVM::AbstractSyntaxTree.parse("_ = #{rb}").children[2] - assert_equal RBS::Parser.parse_type(rbs), parser.literal_to_type(node.children[1]) + node = Prism.parse(rb).value.statements.body[0] + assert_equal RBS::Parser.parse_type(rbs), visitor.literal_to_type(node) end end def test_const_to_name - parser = RBS::Prototype::RB.new + visitor = RB::Visitor.new([]) [ ["self", RBS::TypeName.parse("::Foo")], ["Bar", RBS::TypeName.parse("Bar")], @@ -997,50 +1014,82 @@ def test_const_to_name ["Bar::Baz", RBS::TypeName.parse("Bar::Baz")], ["obj::Baz", nil], ].each do |rb, name| - node = RubyVM::AbstractSyntaxTree.parse("_ = #{rb}").children[2] + node = Prism.parse(rb).value.statements.body[0] context = RBS::Prototype::RB::Context.initial(namespace: RBS::Namespace.new(path: [:Foo], absolute: true)) - assert_equal name, parser.const_to_name(node.children[1], context: context) + assert_equal name, visitor.const_to_name(node, context: context) end end def test_argument_forwarding - parser = RB.new - rb = <<~'RUBY' -module M -def foo(...) end -end - RUBY + module M + def block_unused(...) end - if RUBY_VERSION < '3.4' - # Ruby <=3.3 generates AST without kwrest args for `...` args - assert_write parser.parse(rb), <<~RBS - module M - def foo: (*untyped) ?{ (?) -> untyped } -> nil + def block_optional(...) + yield 1 if block_given? end - RBS - else - # Ruby 3.4 generates AST with kwrest args for `...` args - assert_write parser.parse(rb), <<~RBS + end + RUBY + + assert_write RB.parse(rb), <<~RBS module M - def foo: (*untyped, **untyped) ?{ (?) -> untyped } -> nil + def block_unused: (*untyped, **untyped) ?{ (?) -> untyped } -> nil + + def block_optional: (*untyped, **untyped) ?{ (untyped) -> untyped } -> (untyped | nil) end RBS - end end def test_endless_method_definition - parser = RB.new rb = <<~'RUBY' module M def foo = 42 end RUBY - assert_write parser.parse(rb), <<~RBS + assert_write RB.parse(rb), <<~RBS module M def foo: () -> 42 end RBS end + + def test_ignores_anonymous_modules + rb = <<~RUBY + Module.new do + def hook(name, path) + super + end + end + RUBY + + assert_write RB.parse(rb), '' + end + + def test_anonymous_block + rb = <<~RUBY + module M + def block_optional(&) + yield if block_given? + end + + def block_required(&) + yield + end + + def block_unused(&) + end + end + RUBY + + assert_write RB.parse(rb), <<~RBS + module M + def block_optional: () ?{ () -> untyped } -> (untyped | nil) + + def block_required: () { () -> untyped } -> untyped + + def block_unused: () { (?) -> untyped } -> nil + end + RBS + end end From 46d395ea7967211ea0fa4f0a55f4333787bf0880 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:08:01 +0200 Subject: [PATCH 3/4] Fix handling of empty array optional argument rb prototype The rubyvm code only handled `LIST` but should have checked `ZLIST` as well. As such the type was untyped previously --- lib/rbs/prototype/rb.rb | 7 +------ test/rbs/rb_prototype_test.rb | 4 ++++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/rbs/prototype/rb.rb b/lib/rbs/prototype/rb.rb index ad7a7218bc..4a3d1fe830 100644 --- a/lib/rbs/prototype/rb.rb +++ b/lib/rbs/prototype/rb.rb @@ -838,12 +838,7 @@ def param_type(node, default: Types::Bases::Any.new(location: nil)) when Prism::TrueNode, Prism::FalseNode Types::Bases::Bool.new(location: nil) when Prism::ArrayNode - # FIXME bug replicating empty array untyped - if node.elements.any? - BuiltinNames::Array.instance_type(default) - else - default - end + BuiltinNames::Array.instance_type(default) when Prism::HashNode BuiltinNames::Hash.instance_type(default, default) else diff --git a/test/rbs/rb_prototype_test.rb b/test/rbs/rb_prototype_test.rb index 29260f5f0e..c34a63239a 100644 --- a/test/rbs/rb_prototype_test.rb +++ b/test/rbs/rb_prototype_test.rb @@ -49,6 +49,8 @@ def self.world end def kw_req(a:) end + + def empty_array_opt(a = []) end end EOR @@ -59,6 +61,8 @@ def hello: (untyped a, ?::Integer b, *untyped c, untyped d, e: untyped, ?f: ::In def self.world: () { (untyped, untyped, untyped, x: untyped, y: untyped) -> untyped } -> untyped def kw_req: (a: untyped) -> nil + + def empty_array_opt: (?::Array[untyped] a) -> nil end EOF end From 8351f274072b9017c3347933058b2ae32755b357 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:55:10 +0200 Subject: [PATCH 4/4] Fix rb prototype type for return with multiple values For the first case it resovled to untyped For the second case, it was `Array | untyped` In both cases we know it will return an array --- lib/rbs/prototype/rb.rb | 33 +++++++++++++++++++++------------ sig/prototype/rb.rbs | 2 ++ test/rbs/rb_prototype_test.rb | 23 +++++++++++++++++++++++ 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/lib/rbs/prototype/rb.rb b/lib/rbs/prototype/rb.rb index 4a3d1fe830..c6c4a3d8ce 100644 --- a/lib/rbs/prototype/rb.rb +++ b/lib/rbs/prototype/rb.rb @@ -669,6 +669,8 @@ def body_type(node) body_type(node.statements) when Prism::StatementsNode block_type(node) + when Prism::ReturnNode + return_node_to_type(node) else literal_to_type(node) end @@ -678,21 +680,16 @@ def block_type(node) return_stmts = any_node?(node) do |n| n.is_a?(Prism::ReturnNode) end&.map do |return_node| - return_args = return_node.arguments&.arguments || [] - return_types = return_args.map { |arg| literal_to_type(arg) } - - if return_types.size >= 2 - t = types_to_union_type(return_types) - BuiltinNames::Array.instance_type(t) - else - return_types.first || Types::Bases::Nil.new(location: nil) - end + return_node_to_type(return_node) end || [] last_node = node.compact_child_nodes.last - last_evaluated = last_node ? literal_to_type(last_node) : Types::Bases::Nil.new(location: nil) - # FIXME: Skipt if last_evaluated ReturnNode - types_to_union_type([*return_stmts, last_evaluated]) + case last_node + when nil, Prism::ReturnNode + types_to_union_type(return_stmts) + else + types_to_union_type([*return_stmts, literal_to_type(last_node)]) + end end def literal_to_symbol(node) @@ -784,6 +781,18 @@ def literal_to_type(node) end end + def return_node_to_type(node) + return_args = node.arguments&.arguments || [] + return_types = return_args.map { |arg| literal_to_type(arg) } + + if return_types.size >= 2 + t = types_to_union_type(return_types) + BuiltinNames::Array.instance_type(t) + else + return_types.first || Types::Bases::Nil.new(location: nil) + end + end + def types_to_union_type(types) return untyped if types.empty? diff --git a/sig/prototype/rb.rbs b/sig/prototype/rb.rbs index 7479fd0793..36544fd9b8 100644 --- a/sig/prototype/rb.rbs +++ b/sig/prototype/rb.rbs @@ -62,6 +62,8 @@ module RBS def block_type: (Prism::Node node) -> Types::t + def return_node_to_type: (Prism::ReturnNode) -> Types::t + def literal_to_type: (Prism::Node node) -> Types::t def types_to_union_type: (Array[Types::t] types) -> Types::t diff --git a/test/rbs/rb_prototype_test.rb b/test/rbs/rb_prototype_test.rb index c34a63239a..626ee26dae 100644 --- a/test/rbs/rb_prototype_test.rb +++ b/test/rbs/rb_prototype_test.rb @@ -324,6 +324,29 @@ def with_unless: () -> (:sym | nil) EOR end + def test_defs_return_type_multiple + rb = <<~RUBY + class Hello + def one_statement + return 1, "" + end + + def multiple_statements + foo + return 1, "" + end + end + RUBY + + assert_write RB.parse(rb), <<~RBS + class Hello + def one_statement: () -> ::Array[1 | \"\"] + + def multiple_statements: () -> ::Array[1 | \"\"] + end + RBS + end + def test_sclass rb = <<-EOR class Hello