diff --git a/lib/rbs/cli.rb b/lib/rbs/cli.rb index c645c2deb..8a53e9301 100644 --- a/lib/rbs/cli.rb +++ b/lib/rbs/cli.rb @@ -108,10 +108,6 @@ def parse_logging_options(opts) opts end - def has_parser? - defined?(RubyVM::AbstractSyntaxTree) ? true : false - end - def run(args) @original_args = args.dup @@ -683,10 +679,6 @@ def autoload(name, path) end def run_prototype_file(format, args) - availability = unless has_parser? - "\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? @@ -697,7 +689,7 @@ def run_prototype_file(format, args) opts = OptionParser.new opts.banner = < do case format when "rbi" - Prototype::RBI.new() + Prototype::RBI when "rb" - Prototype::RB.new() + Prototype::RB else raise end @@ -796,7 +783,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 +804,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 +824,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..a22785455 100644 --- a/lib/rbs/prototype/helpers.rb +++ b/lib/rbs/prototype/helpers.rb @@ -5,160 +5,27 @@ module Prototype module Helpers private - 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 - end - 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 + 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 - function = Types::UntypedFunction.new(return_type: untyped) + hash[line] = comment 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 2c5f07e0b..09f810869 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 8e3562db4..c6c4a3d8c 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,51 +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 = [] + class Visitor < Prism::Visitor + attr_reader :context + attr_reader :comments + attr_reader :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) - - 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 - 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) @@ -92,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 ) @@ -180,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)) @@ -210,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 @@ -222,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 @@ -234,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 @@ -275,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 @@ -297,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 @@ -328,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) @@ -357,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. @@ -385,431 +399,484 @@ 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 - def body_type(node) - return Types::Bases::Nil.new(location: nil) unless node + 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 - case node.type - when :IF, :UNLESS - if_unless_type(node) - when :BLOCK - block_type(node) - else - literal_to_type(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 + + 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) + 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 + 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) + when Prism::ReturnNode + return_node_to_type(node) else - BuiltinNames::String.instance_type + literal_to_type(node) 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 block_type(node) + return_stmts = any_node?(node) do |n| + n.is_a?(Prism::ReturnNode) + end&.map do |return_node| + return_node_to_type(return_node) + end || [] + + last_node = node.compact_child_nodes.last + case last_node + when nil, Prism::ReturnNode + types_to_union_type(return_stmts) else - BuiltinNames::Symbol.instance_type + types_to_union_type([*return_stmts, literal_to_type(last_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 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_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 - key_types << untyped - value_types << untyped + untyped end + 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) + 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 - untyped + return_types.first || Types::Bases::Nil.new(location: nil) 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 + end - uniq = types.uniq - if uniq.size == 1 - return uniq.first || raise + 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 + BuiltinNames::Array.instance_type(default) + 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/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/lib/rbs/prototype/runtime.rb b/lib/rbs/prototype/runtime.rb index 6cf73b879..9ee2fa75d 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 c631ed821..075c59017 100644 --- a/sig/cli.rbs +++ b/sig/cli.rbs @@ -44,8 +44,6 @@ module RBS def parse_logging_options: (OptionParser) -> void - def has_parser?: () -> 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 d8bbf54cc..b14989236 100644 --- a/sig/prototype/helpers.rbs +++ b/sig/prototype/helpers.rbs @@ -1,29 +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 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 + def process_comments: (Array[Prism::Comment] comments, include_trailing: bool) -> Hash[Integer, AST::Comment] end end end diff --git a/sig/prototype/node_usage.rbs b/sig/prototype/node_usage.rbs index 6a72f8346..535cf108d 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 e575fc4ee..36544fd9b 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,78 @@ 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) -> void + 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 return_node_to_type: (Prism::ReturnNode) -> Types::t - def types_to_union_type: (Array[Types::t] types) -> Types::t + def literal_to_type: (Prism::Node node) -> Types::t - def range_element_type: (Array[Types::t] types) -> Types::t + def types_to_union_type: (Array[Types::t] types) -> Types::t - def param_type: (RubyVM::AbstractSyntaxTree::Node node, ?default: Types::Bases::Any) -> Types::t + def range_element_type: (Array[Types::t] types) -> Types::t - # backward compatible - alias node_type param_type + def param_type: (Prism::Node node, ?default: Types::Bases::Any) -> Types::t - def private: () -> AST::Members::Private + def any_node?: (Prism::Node node, ?nodes: Array[Prism::Node]) { (Prism::Node) -> bool } -> untyped - @private: AST::Members::Private? + def keyword_hash?: (Prism::Node) -> bool - def public: () -> AST::Members::Public + # backward compatible + alias node_type param_type - @public: AST::Members::Public? + def private: () -> AST::Members::Private - def current_accessibility: (Array[decl] decls, ?Integer index) -> (AST::Members::Private | AST::Members::Public) + @private: AST::Members::Private? - def remove_unnecessary_accessibility_methods!: (Array[decl]) -> void + def public: () -> AST::Members::Public - def is_accessibility?: (decl) -> bool + @public: AST::Members::Public? - def find_def_index_by_name: (Array[decl] decls, Symbol name) -> [Integer, AST::Members::MethodDefinition | AST::Members::AttrReader | AST::Members::AttrWriter]? + def untyped: () -> Types::Bases::Any - def sort_members!: (Array[decl] decls) -> void + @untyped: Types::Bases::Any? + + 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/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/sig/prototype/runtime.rbs b/sig/prototype/runtime.rbs index 7d9464dfb..57d9fb6ec 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 3d37fe9c0..958494eb0 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? - 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 7f20677af..a10596388 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 d628bec0d..626ee26da 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,9 +20,7 @@ class Bar < Struct.new(:bar) end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello end @@ -43,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) @@ -58,25 +49,25 @@ def self.world end def kw_req(a:) end + + def empty_array_opt(a = []) end end EOR - parser.parse(rb) - - assert_write parser.decls, <<-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 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 def test_defs_return_type - parser = RB.new - rb = <<-'EOR' class Hello def initialize() 'foo' end @@ -119,9 +110,7 @@ def self1() self end end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello def initialize: () -> void @@ -183,8 +172,6 @@ def self1: () -> self end def test_defs_return_type_with_block - parser = RB.new - rb = <<-'EOR' class Hello def with_return @@ -219,9 +206,7 @@ def when_last_is_nil end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello def with_return: () -> (1 | "2" | :x) @@ -237,8 +222,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 @@ -257,9 +240,7 @@ def with_optional_block2(&block) end EOR - parser.parse(rb) - - assert_write parser.decls, <<~EOF + assert_write RB.parse(rb), <<~EOF class Hello def with_optional_block1: () ?{ (untyped) -> untyped } -> (untyped | nil) @@ -269,8 +250,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 @@ -328,9 +307,7 @@ def with_unless end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOR + assert_write RB.parse(rb), <<-EOR class ReturnTypeWithIF def with_if: () -> (true | nil) @@ -347,9 +324,30 @@ def with_unless: () -> (:sym | nil) EOR end - def test_sclass - parser = RB.new + 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 class << self @@ -358,9 +356,7 @@ def hello() end end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello def self.hello: () -> nil end @@ -368,8 +364,6 @@ def self.hello: () -> nil end def test_meta_programming - parser = RB.new - rb = <<-EOR class Hello include Foo @@ -398,9 +392,7 @@ module Mod2 end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello include Foo @@ -442,8 +434,6 @@ module Mod2 end def test_module_function - parser = RB.new - rb = <<-EOR module Hello def foo() end @@ -461,9 +451,7 @@ def foobar() end end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF module Hello def foo: () -> nil @@ -477,8 +465,6 @@ def self?.foobar: () -> nil end def test_accessibility - parser = RB.new - rb = <<-EOR class Hello attr_reader :private_attr @@ -505,9 +491,7 @@ def prv4() end end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello private @@ -537,8 +521,6 @@ def prv4: () -> nil end def test_accessibility_and_sclass - parser = RB.new - rb = <<~RUBY class C class << self @@ -551,9 +533,7 @@ def bar() end end RUBY - parser.parse(rb) - - assert_write parser.decls, <<~RBS + assert_write RB.parse(rb), <<~RBS class C private @@ -567,8 +547,6 @@ def bar: () -> nil end def test_aliases - parser = RB.new - rb = <<-EOR class Hello alias a b @@ -584,9 +562,7 @@ class << self end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello alias a b @@ -600,8 +576,6 @@ class Hello end def test_comments - parser = RB.new - rb = <<-EOR # Comments for class. # This is a comment. @@ -633,9 +607,7 @@ def self.world end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF # Comments for class. # This is a comment. class Hello @@ -667,16 +639,12 @@ def self.world: () -> nil end def test_toplevel - parser = RB.new - rb = <<-EOR def hello end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF class Object def hello: () -> nil end @@ -684,8 +652,6 @@ def hello: () -> nil end def test_const - parser = RB.new - rb = <<-EOR module Foo VERSION = '0.1.1' @@ -695,9 +661,7 @@ module Foo end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF module Foo VERSION: "0.1.1" @@ -711,17 +675,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 - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF module Foo MAJOR: untyped @@ -732,9 +692,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 @@ -747,9 +723,7 @@ def test_literal_types I = self EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF A: 1 B: ::Float @@ -771,14 +745,11 @@ def test_literal_types end 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 RB.parse(rb), "A: ::String\n" end - def test_argumentless_fcall - parser = RB.new - + def test_argumentless_call rb = <<-'EOR' class C included do @@ -787,17 +758,13 @@ class C end EOR - parser.parse(rb) - - assert_write parser.decls, <<-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 @@ -805,9 +772,7 @@ class C end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF class C def foo: () -> nil end @@ -815,8 +780,6 @@ def foo: () -> nil end def test_multiple_nested_class - parser = RB.new - rb = <<-'EOR' module Foo class Bar @@ -829,9 +792,7 @@ class Baz end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF module Foo class Bar end @@ -845,8 +806,6 @@ class Baz end def test_duplicate_methods - parser = RB.new - rb = <<-'EOR' class C def foo(x, y, z) @@ -855,18 +814,14 @@ def foo(x, y, z) end EOR - parser.parse(rb) - - assert_write parser.decls, <<-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 @@ -884,9 +839,7 @@ def in_included end RUBY - parser.parse(rb) - - assert_write parser.decls, <<~RBS + assert_write RB.parse(rb), <<~RBS module M def not_refinements: () -> nil end @@ -894,8 +847,6 @@ def not_refinements: () -> nil end def test_calling_class_method_from_instance - parser = RB.new - rb = <<~'RUBY' class HelloWorld def self.world(str) @@ -908,9 +859,7 @@ def hello end RUBY - parser.parse(rb) - - assert_write parser.decls, <<~RBS + assert_write RB.parse(rb), <<~RBS class HelloWorld def self.world: (untyped str) -> untyped @@ -920,32 +869,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 - parser.parse(rb) - - assert_write parser.decls, <<-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) @@ -959,9 +912,7 @@ def foo end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF module Hello # comment for ivar @message: untyped @@ -975,9 +926,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 @@ -1001,9 +968,7 @@ def message(message) end EOR - parser.parse(rb) - - assert_write parser.decls, <<-EOF + assert_write RB.parse(rb), <<-EOF class Hello # comment for cvar @@message: untyped @@ -1023,25 +988,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")], @@ -1049,53 +1041,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 - - parser.parse(rb) + module M + def block_unused(...) end - if RUBY_VERSION < '3.4' - # Ruby <=3.3 generates AST without kwrest args for `...` args - assert_write parser.decls, <<~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.decls, <<~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 - parser.parse(rb) - assert_write parser.decls, <<~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 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