diff --git a/lib/rbs/cli.rb b/lib/rbs/cli.rb index c645c2deb..244f51a65 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 f508f45c5..a1869559d 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 8e3562db4..371110969 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 169e91042..794212bfe 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 c631ed821..5b56646ac 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 d8bbf54cc..634c71213 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 e575fc4ee..7b5bda5e5 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 2228f0cfb..e82894714 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 3d37fe9c0..d8407d6be 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 d628bec0d..afe5d5c79 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 c65cc535a..e33307be8 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