lib/rbi/parser.rb in rbi-0.0.17 vs lib/rbi/parser.rb in rbi-0.1.0
- old
+ new
@@ -1,9 +1,9 @@
# typed: strict
# frozen_string_literal: true
-require "parser"
+require "yarp"
module RBI
class ParseError < StandardError
extend T::Sig
@@ -51,23 +51,10 @@
end
class Parser
extend T::Sig
- # opt-in to most recent AST format
- ::Parser::Builders::Default.emit_lambda = true
- ::Parser::Builders::Default.emit_procarg0 = true
- ::Parser::Builders::Default.emit_encoding = true
- ::Parser::Builders::Default.emit_index = true
- ::Parser::Builders::Default.emit_arg_inside_procarg0 = true
-
- sig { void }
- def initialize
- # Delay load unparser and only if it has not been loaded already.
- require "unparser" unless defined?(::Unparser)
- end
-
class << self
extend T::Sig
sig { params(string: String).returns(Tree) }
def parse_string(string)
@@ -102,657 +89,721 @@
parse(::File.read(path), file: path)
end
private
- sig { params(content: String, file: String).returns(Tree) }
- def parse(content, file:)
- node, comments = Unparser.parse_with_comments(content)
- assoc = ::Parser::Source::Comment.associate_locations(node, comments)
- builder = TreeBuilder.new(file: file, comments: comments, nodes_comments_assoc: assoc)
- builder.visit(node)
- builder.post_process
- builder.tree
- rescue ::Parser::SyntaxError => e
- raise ParseError.new(e.message, Loc.from_ast_loc(file, e.diagnostic.location))
+ sig { params(source: String, file: String).returns(Tree) }
+ def parse(source, file:)
+ result = YARP.parse(source)
+ unless result.success?
+ raise ParseError.new(result.errors.map(&:message).join(" "), Loc.from_yarp(file, result.errors.first.location))
+ end
+
+ visitor = TreeBuilder.new(source, comments: result.comments, file: file)
+ visitor.visit(result.value)
+ visitor.tree
+ rescue YARP::ParseError => e
+ raise ParseError.new(e.message, Loc.from_yarp(file, e.location))
rescue ParseError => e
raise e
rescue => e
- last_node = builder&.last_node
+ last_node = visitor&.last_node
last_location = if last_node
- Loc.from_ast_loc(file, last_node.location)
+ Loc.from_yarp(file, last_node.location)
else
Loc.new(file: file)
end
exception = UnexpectedParserError.new(e, last_location)
exception.print_debug
raise exception
end
- end
- class ASTVisitor
- extend T::Helpers
- extend T::Sig
+ class Visitor < YARP::Visitor
+ extend T::Sig
- abstract!
+ sig { params(source: String, file: String).void }
+ def initialize(source, file:)
+ super()
- sig { params(nodes: T::Array[AST::Node]).void }
- def visit_all(nodes)
- nodes.each { |node| visit(node) }
- end
+ @source = source
+ @file = file
+ end
- sig { abstract.params(node: T.nilable(AST::Node)).void }
- def visit(node); end
+ private
- private
+ sig { params(node: YARP::Node).returns(Loc) }
+ def node_loc(node)
+ Loc.from_yarp(@file, node.location)
+ end
- sig { params(node: AST::Node).returns(String) }
- def parse_name(node)
- T.must(ConstBuilder.visit(node))
- end
+ sig { params(node: T.nilable(YARP::Node)).returns(T.nilable(String)) }
+ def node_string(node)
+ return unless node
- sig { params(node: AST::Node).returns(String) }
- def parse_expr(node)
- Unparser.unparse(node)
+ node.slice
+ end
+
+ sig { params(node: YARP::Node).returns(String) }
+ def node_string!(node)
+ T.must(node_string(node))
+ end
end
- end
- class TreeBuilder < ASTVisitor
- extend T::Sig
+ class TreeBuilder < Visitor
+ extend T::Sig
- sig { returns(Tree) }
- attr_reader :tree
+ sig { returns(Tree) }
+ attr_reader :tree
- sig { returns(T.nilable(::AST::Node)) }
- attr_reader :last_node
+ sig { returns(T.nilable(YARP::Node)) }
+ attr_reader :last_node
- sig do
- params(
- file: String,
- comments: T::Array[::Parser::Source::Comment],
- nodes_comments_assoc: T::Hash[::Parser::Source::Map, T::Array[::Parser::Source::Comment]],
- ).void
- end
- def initialize(file:, comments: [], nodes_comments_assoc: {})
- super()
- @file = file
- @comments = comments
- @nodes_comments_assoc = nodes_comments_assoc
- @tree = T.let(Tree.new, Tree)
- @scopes_stack = T.let([@tree], T::Array[Tree])
- @last_node = T.let(nil, T.nilable(::AST::Node))
- @last_sigs = T.let([], T::Array[RBI::Sig])
- @last_sigs_comments = T.let([], T::Array[Comment])
+ sig { params(source: String, comments: T::Array[YARP::Comment], file: String).void }
+ def initialize(source, comments:, file:)
+ super(source, file: file)
- separate_header_comments
- end
+ @comments_by_line = T.let(comments.to_h { |c| [c.location.start_line, c] }, T::Hash[Integer, YARP::Comment])
+ @tree = T.let(Tree.new, Tree)
- sig { void }
- def post_process
- assoc_dangling_comments
- set_root_tree_loc
- end
+ @scopes_stack = T.let([@tree], T::Array[Tree])
+ @last_node = T.let(nil, T.nilable(YARP::Node))
+ @last_sigs = T.let([], T::Array[RBI::Sig])
+ @last_sigs_comments = T.let([], T::Array[Comment])
+ end
- sig { override.params(node: T.nilable(Object)).void }
- def visit(node)
- return unless node.is_a?(AST::Node)
+ sig { override.params(node: T.nilable(YARP::Node)).void }
+ def visit(node)
+ return unless node
- @last_node = node
+ @last_node = node
+ super
+ end
- case node.type
- when :module, :class, :sclass
- scope = parse_scope(node)
+ sig { override.params(node: YARP::ClassNode).void }
+ def visit_class_node(node)
+ scope = Class.new(
+ node_string!(node.constant_path),
+ superclass_name: node_string(node.superclass),
+ loc: node_loc(node),
+ comments: node_comments(node),
+ )
+
current_scope << scope
@scopes_stack << scope
- visit_all(node.children)
+ visit(node.body)
+ collect_dangling_comments(node)
@scopes_stack.pop
- when :casgn
- current_scope << parse_const_assign(node)
- when :def, :defs
- current_scope << parse_def(node)
- when :send
- node = parse_send(node)
- current_scope << node if node
- when :block
- rbi_node = parse_block(node)
- if rbi_node.is_a?(Sig)
- @last_sigs << rbi_node
- @last_sigs_comments.concat(node_comments(node))
- elsif rbi_node
- current_scope << rbi_node
- end
- else
- visit_all(node.children)
end
- @last_node = nil
- end
-
- private
-
- sig { params(node: AST::Node).returns(Scope) }
- def parse_scope(node)
- loc = node_loc(node)
- comments = node_comments(node)
-
- case node.type
- when :module
- name = parse_name(node.children[0])
- Module.new(name, loc: loc, comments: comments)
- when :class
- name = parse_name(node.children[0])
- superclass_name = ConstBuilder.visit(node.children[1])
- Class.new(name, superclass_name: superclass_name, loc: loc, comments: comments)
- when :sclass
- SingletonClass.new(loc: loc, comments: comments)
- else
- raise ParseError.new("Unsupported scope node type `#{node.type}`", loc)
+ sig { override.params(node: YARP::ConstantWriteNode).void }
+ def visit_constant_write_node(node)
+ visit_constant_assign(node)
end
- end
- sig { params(node: AST::Node).returns(RBI::Node) }
- def parse_const_assign(node)
- node_value = node.children[2]
- if struct_definition?(node_value)
- parse_struct(node)
- elsif type_variable_definition?(node_value)
- parse_type_variable(node)
- else
- name = parse_name(node)
- value = parse_expr(node_value)
- loc = node_loc(node)
- comments = node_comments(node)
- Const.new(name, value, loc: loc, comments: comments)
+ sig { override.params(node: YARP::ConstantPathWriteNode).void }
+ def visit_constant_path_write_node(node)
+ visit_constant_assign(node)
end
- end
- sig { params(node: AST::Node).returns(Method) }
- def parse_def(node)
- loc = node_loc(node)
+ sig { params(node: T.any(YARP::ConstantWriteNode, YARP::ConstantPathWriteNode)).void }
+ def visit_constant_assign(node)
+ struct = parse_struct(node)
- case node.type
- when :def
- Method.new(
- node.children[0].to_s,
- params: node.children[1].children.map { |child| parse_param(child) },
+ current_scope << if struct
+ struct
+ elsif type_variable_definition?(node.value)
+ TypeMember.new(
+ case node
+ when YARP::ConstantWriteNode
+ node.name
+ when YARP::ConstantPathWriteNode
+ node_string!(node.target)
+ end,
+ node_string!(node.value),
+ loc: node_loc(node),
+ comments: node_comments(node),
+ )
+ else
+ Const.new(
+ case node
+ when YARP::ConstantWriteNode
+ node.name
+ when YARP::ConstantPathWriteNode
+ node_string!(node.target)
+ end,
+ node_string!(node.value),
+ loc: node_loc(node),
+ comments: node_comments(node),
+ )
+ end
+ end
+
+ sig { override.params(node: YARP::DefNode).void }
+ def visit_def_node(node)
+ current_scope << Method.new(
+ node.name,
+ params: parse_params(node.parameters),
sigs: current_sigs,
- loc: loc,
+ loc: node_loc(node),
comments: current_sigs_comments + node_comments(node),
+ is_singleton: !!node.receiver,
)
- when :defs
- Method.new(
- node.children[1].to_s,
- params: node.children[2].children.map { |child| parse_param(child) },
- is_singleton: true,
- sigs: current_sigs,
- loc: loc,
- comments: current_sigs_comments + node_comments(node),
- )
- else
- raise ParseError.new("Unsupported def node type `#{node.type}`", loc)
end
- end
- sig { params(node: AST::Node).returns(Param) }
- def parse_param(node)
- name = node.children[0].to_s
- loc = node_loc(node)
- comments = node_comments(node)
+ sig { override.params(node: YARP::ModuleNode).void }
+ def visit_module_node(node)
+ scope = Module.new(
+ node_string!(node.constant_path),
+ loc: node_loc(node),
+ comments: node_comments(node),
+ )
- case node.type
- when :arg
- ReqParam.new(name, loc: loc, comments: comments)
- when :optarg
- value = parse_expr(node.children[1])
- OptParam.new(name, value, loc: loc, comments: comments)
- when :restarg
- RestParam.new(name, loc: loc, comments: comments)
- when :kwarg
- KwParam.new(name, loc: loc, comments: comments)
- when :kwoptarg
- value = parse_expr(node.children[1])
- KwOptParam.new(name, value, loc: loc, comments: comments)
- when :kwrestarg
- KwRestParam.new(name, loc: loc, comments: comments)
- when :blockarg
- BlockParam.new(name, loc: loc, comments: comments)
- else
- raise ParseError.new("Unsupported param node type `#{node.type}`", loc)
+ current_scope << scope
+ @scopes_stack << scope
+ visit(node.body)
+ collect_dangling_comments(node)
+ @scopes_stack.pop
end
- end
- sig { params(node: AST::Node).returns(T.nilable(RBI::Node)) }
- def parse_send(node)
- recv = node.children[0]
- return if recv && recv != :self
+ sig { override.params(node: YARP::ProgramNode).void }
+ def visit_program_node(node)
+ super
- method_name = node.children[1]
- loc = node_loc(node)
- comments = node_comments(node)
+ collect_orphan_comments
+ separate_header_comments
+ set_root_tree_loc
+ end
- case method_name
- when :attr_reader
- symbols = node.children[2..-1].map { |child| child.children[0] }
- AttrReader.new(*symbols, sigs: current_sigs, loc: loc, comments: current_sigs_comments + comments)
- when :attr_writer
- symbols = node.children[2..-1].map { |child| child.children[0] }
- AttrWriter.new(*symbols, sigs: current_sigs, loc: loc, comments: current_sigs_comments + comments)
- when :attr_accessor
- symbols = node.children[2..-1].map { |child| child.children[0] }
- AttrAccessor.new(*symbols, sigs: current_sigs, loc: loc, comments: current_sigs_comments + comments)
- when :include
- names = node.children[2..-1].map { |child| parse_expr(child) }
- Include.new(*names, loc: loc, comments: comments)
- when :extend
- names = node.children[2..-1].map { |child| parse_expr(child) }
- Extend.new(*names, loc: loc, comments: comments)
- when :abstract!, :sealed!, :interface!
- Helper.new(method_name.to_s.delete_suffix("!"), loc: loc, comments: comments)
- when :mixes_in_class_methods
- names = node.children[2..-1].map { |child| parse_name(child) }
- MixesInClassMethods.new(*names, loc: loc, comments: comments)
- when :public, :protected, :private
- visibility = case method_name
- when :protected
- Protected.new(loc: loc)
- when :private
- Private.new(loc: loc)
- else
- Public.new(loc: loc)
- end
- nested_node = node.children[2]
- case nested_node&.type
- when :def, :defs
- method = parse_def(nested_node)
- method.visibility = visibility
- method
- when :send
- snode = parse_send(nested_node)
- raise ParseError.new("Unexpected token `private` before `#{nested_node.type}`", loc) unless snode.is_a?(Attr)
+ sig { override.params(node: YARP::SingletonClassNode).void }
+ def visit_singleton_class_node(node)
+ scope = SingletonClass.new(
+ loc: node_loc(node),
+ comments: node_comments(node),
+ )
- snode.visibility = visibility
- snode
- when nil
- visibility
- else
- raise ParseError.new("Unexpected token `private` before `#{nested_node.type}`", loc)
- end
- when :prop
- name, type, default_value = parse_tstruct_prop(node)
- TStructProp.new(name, type, default: default_value, loc: loc, comments: comments)
- when :const
- name, type, default_value = parse_tstruct_prop(node)
- TStructConst.new(name, type, default: default_value, loc: loc, comments: comments)
- else
- args = parse_send_args(node)
- Send.new(method_name.to_s, args, loc: loc, comments: comments)
+ current_scope << scope
+ @scopes_stack << scope
+ visit(node.body)
+ collect_dangling_comments(node)
+ @scopes_stack.pop
end
- end
- sig { params(node: AST::Node).returns(T::Array[Arg]) }
- def parse_send_args(node)
- args = T.let([], T::Array[Arg])
- node.children[2..-1].each do |child|
- if child.type == :kwargs
- child.children.each do |pair|
- keyword = pair.children.first.children.last.to_s
- value = parse_expr(pair.children.last)
- args << KwArg.new(keyword, value)
+ sig { params(node: YARP::CallNode).void }
+ def visit_call_node(node)
+ message = node.name
+ case message
+ when "abstract!", "sealed!", "interface!"
+ current_scope << Helper.new(
+ message.delete_suffix("!"),
+ loc: node_loc(node),
+ comments: node_comments(node),
+ )
+ when "attr_reader"
+ args = node.arguments
+ return unless args.is_a?(YARP::ArgumentsNode) && args.arguments.any?
+
+ current_scope << AttrReader.new(
+ *T.unsafe(args.arguments.map { |arg| node_string!(arg).delete_prefix(":").to_sym }),
+ sigs: current_sigs,
+ loc: node_loc(node),
+ comments: current_sigs_comments + node_comments(node),
+ )
+ when "attr_writer"
+ args = node.arguments
+ return unless args.is_a?(YARP::ArgumentsNode) && args.arguments.any?
+
+ current_scope << AttrWriter.new(
+ *T.unsafe(args.arguments.map { |arg| node_string!(arg).delete_prefix(":").to_sym }),
+ sigs: current_sigs,
+ loc: node_loc(node),
+ comments: current_sigs_comments + node_comments(node),
+ )
+ when "attr_accessor"
+ args = node.arguments
+ return unless args.is_a?(YARP::ArgumentsNode) && args.arguments.any?
+
+ current_scope << AttrAccessor.new(
+ *T.unsafe(args.arguments.map { |arg| node_string!(arg).delete_prefix(":").to_sym }),
+ sigs: current_sigs,
+ loc: node_loc(node),
+ comments: current_sigs_comments + node_comments(node),
+ )
+ when "enums"
+ block = node.block
+ return unless block.is_a?(YARP::BlockNode)
+
+ body = block.body
+ return unless body.is_a?(YARP::StatementsNode)
+
+ current_scope << TEnumBlock.new(
+ body.body.map { |stmt| T.cast(stmt, YARP::ConstantWriteNode).name },
+ loc: node_loc(node),
+ comments: node_comments(node),
+ )
+ when "extend"
+ args = node.arguments
+ return unless args.is_a?(YARP::ArgumentsNode) && args.arguments.any?
+
+ current_scope << Extend.new(
+ *T.unsafe(args.arguments.map { |arg| node_string!(arg) }),
+ loc: node_loc(node),
+ comments: node_comments(node),
+ )
+ when "include"
+ args = node.arguments
+ return unless args.is_a?(YARP::ArgumentsNode) && args.arguments.any?
+
+ current_scope << Include.new(
+ *T.unsafe(args.arguments.map { |arg| node_string!(arg) }),
+ loc: node_loc(node),
+ comments: node_comments(node),
+ )
+ when "mixes_in_class_methods"
+ args = node.arguments
+ return unless args.is_a?(YARP::ArgumentsNode) && args.arguments.any?
+
+ current_scope << MixesInClassMethods.new(
+ *T.unsafe(args.arguments.map { |arg| node_string!(arg) }),
+ loc: node_loc(node),
+ comments: node_comments(node),
+ )
+ when "private", "protected", "public"
+ args = node.arguments
+ if args.is_a?(YARP::ArgumentsNode) && args.arguments.any?
+ visit(node.arguments)
+ last_node = @scopes_stack.last&.nodes&.last
+ case last_node
+ when Method, Attr
+ last_node.visibility = parse_visibility(node.name, node)
+ else
+ raise ParseError.new(
+ "Unexpected token `#{node.message}` before `#{last_node&.string&.strip}`",
+ node_loc(node),
+ )
+ end
+ else
+ current_scope << parse_visibility(node.name, node)
end
+ when "prop", "const"
+ parse_tstruct_field(node)
+ when "requires_ancestor"
+ block = node.block
+ return unless block.is_a?(YARP::BlockNode)
+
+ body = block.body
+ return unless body.is_a?(YARP::StatementsNode)
+
+ current_scope << RequiresAncestor.new(
+ node_string!(body),
+ loc: node_loc(node),
+ comments: node_comments(node),
+ )
+ when "sig"
+ @last_sigs << parse_sig(node)
else
- args << Arg.new(parse_expr(child))
+ current_scope << Send.new(
+ message,
+ parse_send_args(node.arguments),
+ loc: node_loc(node),
+ comments: node_comments(node),
+ )
end
end
- args
- end
- sig { params(node: AST::Node).returns(T.nilable(RBI::Node)) }
- def parse_block(node)
- name = node.children[0].children[1]
+ private
- case name
- when :sig
- parse_sig(node)
- when :enums
- parse_enum(node)
- when :requires_ancestor
- parse_requires_ancestor(node)
- else
- raise ParseError.new("Unsupported block node type `#{name}`", node_loc(node))
- end
- end
+ # Collect all the remaining comments within a node
+ sig { params(node: YARP::Node).void }
+ def collect_dangling_comments(node)
+ first_line = node.location.start_line
+ last_line = node.location.end_line
- sig { params(node: AST::Node).returns(T::Boolean) }
- def struct_definition?(node)
- (node.type == :send && node.children[0]&.type == :const && node.children[0].children[1] == :Struct) ||
- (node.type == :block && struct_definition?(node.children[0]))
- end
+ last_node_last_line = node.child_nodes.last&.location&.end_line
- sig { params(node: AST::Node).returns(RBI::Struct) }
- def parse_struct(node)
- name = parse_name(node)
- loc = node_loc(node)
- comments = node_comments(node)
+ last_line.downto(first_line) do |line|
+ comment = @comments_by_line[line]
+ next unless comment
+ break if last_node_last_line && line <= last_node_last_line
- send = node.children[2]
- body = []
-
- if send.type == :block
- if send.children[2].type == :begin
- body = send.children[2].children
- else
- body << send.children[2]
+ current_scope << parse_comment(comment)
+ @comments_by_line.delete(line)
end
- send = send.children[0]
end
- members = []
- keyword_init = T.let(false, T::Boolean)
- send.children[2..].each do |child|
- if child.type == :sym
- members << child.children[0]
- elsif child.type == :kwargs
- pair = child.children[0]
- if pair.children[0].children[0] == :keyword_init
- keyword_init = true if pair.children[1].type == :true
+ # Collect all the remaining comments after visiting the tree
+ sig { void }
+ def collect_orphan_comments
+ last_line = T.let(nil, T.nilable(Integer))
+ last_node_end = @tree.nodes.last&.loc&.end_line
+
+ @comments_by_line.each do |line, comment|
+ # Associate the comment either with the header or the file or as a dangling comment at the end
+ recv = if last_node_end && line >= last_node_end
+ @tree
+ else
+ @tree.comments
end
+
+ # Preserve blank lines in comments
+ if last_line && line > last_line + 1
+ recv << BlankLine.new(loc: Loc.from_yarp(@file, comment.location))
+ end
+
+ recv << parse_comment(comment)
+ last_line = line
end
end
- struct = Struct.new(name, members: members, keyword_init: keyword_init, loc: loc, comments: comments)
- @scopes_stack << struct
- visit_all(body)
- @scopes_stack.pop
+ sig { returns(Tree) }
+ def current_scope
+ T.must(@scopes_stack.last) # Should never be nil since we create a Tree as the root
+ end
- struct
- end
+ sig { returns(T::Array[Sig]) }
+ def current_sigs
+ sigs = @last_sigs.dup
+ @last_sigs.clear
+ sigs
+ end
- sig { params(node: AST::Node).returns(T::Boolean) }
- def type_variable_definition?(node)
- (node.type == :send && node.children[0].nil? && (node.children[1] == :type_member ||
- node.children[1] == :type_template)) ||
- (node.type == :block && type_variable_definition?(node.children[0]))
- end
+ sig { returns(T::Array[Comment]) }
+ def current_sigs_comments
+ comments = @last_sigs_comments.dup
+ @last_sigs_comments.clear
+ comments
+ end
- sig { params(node: AST::Node).returns(RBI::TypeMember) }
- def parse_type_variable(node)
- name = parse_name(node)
- loc = node_loc(node)
- comments = node_comments(node)
+ sig { params(node: YARP::Node).returns(T::Array[Comment]) }
+ def node_comments(node)
+ comments = []
- send = node.children[2]
+ start_line = node.location.start_line
+ start_line -= 1 unless @comments_by_line.key?(start_line)
- TypeMember.new(name, send.location.expression.source, loc: loc, comments: comments)
- end
+ start_line.downto(1) do |line|
+ comment = @comments_by_line[line]
+ break unless comment
- sig { params(node: AST::Node).returns([String, String, T.nilable(String)]) }
- def parse_tstruct_prop(node)
- name = node.children[2].children[0].to_s
- type = parse_expr(node.children[3])
- has_default = node.children[4]
- &.children&.fetch(0, nil)
- &.children&.fetch(0, nil)
- &.children&.fetch(0, nil) == :default
- default_value = if has_default
- parse_expr(node.children.fetch(4, nil)
- &.children&.fetch(0, nil)
- &.children&.fetch(1, nil))
+ comments.unshift(parse_comment(comment))
+ @comments_by_line.delete(line)
+ end
+
+ comments
end
- [name, type, default_value]
- end
- sig { params(node: AST::Node).returns(Sig) }
- def parse_sig(node)
- sig = SigBuilder.build(node)
- sig.loc = node_loc(node)
- sig
- end
+ sig { params(node: YARP::Comment).returns(Comment) }
+ def parse_comment(node)
+ Comment.new(node.location.slice.gsub(/^# ?/, "").rstrip, loc: Loc.from_yarp(@file, node.location))
+ end
- sig { params(node: AST::Node).returns(TEnumBlock) }
- def parse_enum(node)
- enum = TEnumBlock.new
+ sig { params(node: T.nilable(YARP::Node)).returns(T::Array[Arg]) }
+ def parse_send_args(node)
+ args = T.let([], T::Array[Arg])
+ return args unless node.is_a?(YARP::ArgumentsNode)
- body = if node.children[2].type == :begin
- node.children[2].children
- else
- [node.children[2]]
- end
+ node.arguments.each do |arg|
+ case arg
+ when YARP::KeywordHashNode
+ arg.elements.each do |assoc|
+ next unless assoc.is_a?(YARP::AssocNode)
- body.each do |child|
- enum << parse_name(child)
+ args << KwArg.new(
+ node_string!(assoc.key).delete_suffix(":"),
+ T.must(node_string(assoc.value)),
+ )
+ end
+ else
+ args << Arg.new(T.must(node_string(arg)))
+ end
+ end
+
+ args
end
- enum.loc = node_loc(node)
- enum
- end
- sig { params(node: AST::Node).returns(RequiresAncestor) }
- def parse_requires_ancestor(node)
- name = parse_name(node.children[2])
- ra = RequiresAncestor.new(name)
- ra.loc = node_loc(node)
- ra
- end
+ sig { params(node: T.nilable(YARP::Node)).returns(T::Array[Param]) }
+ def parse_params(node)
+ params = []
+ return params unless node.is_a?(YARP::ParametersNode)
- sig { params(node: AST::Node).returns(Loc) }
- def node_loc(node)
- Loc.from_ast_loc(@file, node.location)
- end
+ node.requireds.each do |param|
+ next unless param.is_a?(YARP::RequiredParameterNode)
- sig { params(node: AST::Node).returns(T::Array[Comment]) }
- def node_comments(node)
- comments = @nodes_comments_assoc[node.location]
- return [] unless comments
+ params << ReqParam.new(
+ param.name.to_s,
+ loc: node_loc(param),
+ comments: node_comments(param),
+ )
+ end
- comments.map do |comment|
- text = comment.text[1..-1].strip
- loc = Loc.from_ast_loc(@file, comment.location)
- Comment.new(text, loc: loc)
- end
- end
+ node.optionals.each do |param|
+ next unless param.is_a?(YARP::OptionalParameterNode)
- sig { returns(Tree) }
- def current_scope
- T.must(@scopes_stack.last) # Should never be nil since we create a Tree as the root
- end
+ params << OptParam.new(
+ param.name.to_s,
+ node_string!(param.value),
+ loc: node_loc(param),
+ comments: node_comments(param),
+ )
+ end
- sig { returns(T::Array[Sig]) }
- def current_sigs
- sigs = @last_sigs.dup
- @last_sigs.clear
- sigs
- end
+ rest = node.rest
+ if rest.is_a?(YARP::RestParameterNode)
+ params << RestParam.new(
+ rest.name || "*args",
+ loc: node_loc(rest),
+ comments: node_comments(rest),
+ )
+ end
- sig { returns(T::Array[Comment]) }
- def current_sigs_comments
- comments = @last_sigs_comments.dup
- @last_sigs_comments.clear
- comments
- end
+ node.keywords.each do |param|
+ next unless param.is_a?(YARP::KeywordParameterNode)
- sig { void }
- def assoc_dangling_comments
- last_line = T.let(nil, T.nilable(Integer))
- (@comments - @nodes_comments_assoc.values.flatten).each do |comment|
- comment_line = comment.location.last_line
- text = comment.text[1..-1].strip
- loc = Loc.from_ast_loc(@file, comment.location)
+ value = param.value
+ params << if value
+ KwOptParam.new(
+ param.name.delete_suffix(":"),
+ node_string!(value),
+ loc: node_loc(param),
+ comments: node_comments(param),
+ )
+ else
+ KwParam.new(
+ param.name.delete_suffix(":"),
+ loc: node_loc(param),
+ comments: node_comments(param),
+ )
+ end
+ end
- if last_line && comment_line > last_line + 1
- # Preserve empty lines in file headers
- tree.comments << BlankLine.new(loc: loc)
+ rest_kw = node.keyword_rest
+ if rest_kw.is_a?(YARP::KeywordRestParameterNode)
+ params << KwRestParam.new(
+ rest_kw.name || "**kwargs",
+ loc: node_loc(rest_kw),
+ comments: node_comments(rest_kw),
+ )
end
- tree.comments << Comment.new(text, loc: loc)
- last_line = comment_line
+ block = node.block
+ if block.is_a?(YARP::BlockParameterNode)
+ params << BlockParam.new(
+ block.name || "&block",
+ loc: node_loc(block),
+ comments: node_comments(block),
+ )
+ end
+
+ params
end
- end
- sig { void }
- def separate_header_comments
- return if @nodes_comments_assoc.empty?
+ sig { params(node: YARP::CallNode).returns(Sig) }
+ def parse_sig(node)
+ @last_sigs_comments = node_comments(node)
- keep = []
- node = T.must(@nodes_comments_assoc.keys.first)
- comments = T.must(@nodes_comments_assoc.values.first)
+ builder = SigBuilder.new(@source, file: @file)
+ builder.current.loc = node_loc(node)
+ builder.visit_call_node(node)
+ builder.current
+ end
- last_line = T.let(nil, T.nilable(Integer))
- comments.reverse.each do |comment|
- comment_line = comment.location.last_line
+ sig { params(node: T.any(YARP::ConstantWriteNode, YARP::ConstantPathWriteNode)).returns(T.nilable(Struct)) }
+ def parse_struct(node)
+ send = node.value
+ return unless send.is_a?(YARP::CallNode)
+ return unless send.message == "new"
- break if last_line && comment_line < last_line - 1 ||
- !last_line && comment_line < node.first_line - 1
+ recv = send.receiver
+ return unless recv
+ return unless node_string(recv) =~ /(::)?Struct/
- keep << comment
- last_line = comment_line
- end
+ members = []
+ keyword_init = T.let(false, T::Boolean)
- @nodes_comments_assoc[node] = keep.reverse
- end
+ args = send.arguments
+ if args.is_a?(YARP::ArgumentsNode)
+ args.arguments.each do |arg|
+ case arg
+ when YARP::SymbolNode
+ members << arg.value
+ when YARP::KeywordHashNode
+ arg.elements.each do |assoc|
+ next unless assoc.is_a?(YARP::AssocNode)
- sig { void }
- def set_root_tree_loc
- first_loc = tree.nodes.first&.loc
- last_loc = tree.nodes.last&.loc
+ key = node_string!(assoc.key)
+ val = node_string(assoc.value)
- @tree.loc = Loc.new(
- file: @file,
- begin_line: first_loc&.begin_line || 0,
- begin_column: first_loc&.begin_column || 0,
- end_line: last_loc&.end_line || 0,
- end_column: last_loc&.end_column || 0,
- )
- end
- end
+ keyword_init = val == "true" if key == "keyword_init:"
+ end
+ else
+ raise ParseError.new("Unexpected node type `#{arg.class}`", node_loc(arg))
+ end
+ end
+ end
- class ConstBuilder < ASTVisitor
- extend T::Sig
+ name = case node
+ when YARP::ConstantWriteNode
+ node.name
+ when YARP::ConstantPathWriteNode
+ node_string!(node.target)
+ end
- sig { returns(T::Array[String]) }
- attr_accessor :names
+ loc = node_loc(node)
+ comments = node_comments(node)
+ struct = Struct.new(name, members: members, keyword_init: keyword_init, loc: loc, comments: comments)
+ @scopes_stack << struct
+ visit(send.block)
+ @scopes_stack.pop
+ struct
+ end
- sig { void }
- def initialize
- super
- @names = T.let([], T::Array[String])
- end
+ sig { params(send: YARP::CallNode).void }
+ def parse_tstruct_field(send)
+ args = send.arguments
+ return unless args.is_a?(YARP::ArgumentsNode)
- class << self
- extend T::Sig
+ name_arg, type_arg, *rest = args.arguments
+ return unless name_arg
+ return unless type_arg
- sig { params(node: T.nilable(AST::Node)).returns(T.nilable(String)) }
- def visit(node)
- v = ConstBuilder.new
- v.visit(node)
- return if v.names.empty?
+ name = node_string!(name_arg).delete_prefix(":")
+ type = node_string!(type_arg)
+ loc = node_loc(send)
+ comments = node_comments(send)
+ default_value = T.let(nil, T.nilable(String))
- v.names.join("::")
+ rest&.each do |arg|
+ next unless arg.is_a?(YARP::KeywordHashNode)
+
+ arg.elements.each do |assoc|
+ next unless assoc.is_a?(YARP::AssocNode)
+
+ if node_string(assoc.key) == "default:"
+ default_value = node_string(assoc.value)
+ end
+ end
+ end
+
+ current_scope << case send.message
+ when "const"
+ TStructConst.new(name, type, default: default_value, loc: loc, comments: comments)
+ when "prop"
+ TStructProp.new(name, type, default: default_value, loc: loc, comments: comments)
+ else
+ raise ParseError.new("Unexpected message `#{send.message}`", loc)
+ end
end
- end
- sig { override.params(node: T.nilable(AST::Node)).void }
- def visit(node)
- return unless node
+ sig { params(name: String, node: YARP::Node).returns(Visibility) }
+ def parse_visibility(name, node)
+ case name
+ when "public"
+ Public.new(loc: node_loc(node))
+ when "protected"
+ Protected.new(loc: node_loc(node))
+ when "private"
+ Private.new(loc: node_loc(node))
+ else
+ raise ParseError.new("Unexpected visibility `#{name}`", node_loc(node))
+ end
+ end
- case node.type
- when :const, :casgn
- visit(node.children[0])
- @names << node.children[1].to_s
- when :cbase
- @names << ""
- when :sym
- @names << ":#{node.children[0]}"
+ sig { void }
+ def separate_header_comments
+ current_scope.nodes.dup.each do |child_node|
+ break unless child_node.is_a?(Comment) || child_node.is_a?(BlankLine)
+
+ current_scope.comments << child_node
+ child_node.detach
+ end
end
- end
- end
- class SigBuilder < ASTVisitor
- extend T::Sig
+ sig { void }
+ def set_root_tree_loc
+ first_loc = tree.nodes.first&.loc
+ last_loc = tree.nodes.last&.loc
- sig { returns(Sig) }
- attr_reader :current
+ @tree.loc = Loc.new(
+ file: @file,
+ begin_line: first_loc&.begin_line || 0,
+ begin_column: first_loc&.begin_column || 0,
+ end_line: last_loc&.end_line || 0,
+ end_column: last_loc&.end_column || 0,
+ )
+ end
- sig { void }
- def initialize
- super
- @current = T.let(Sig.new, Sig)
+ sig { params(node: T.nilable(YARP::Node)).returns(T::Boolean) }
+ def type_variable_definition?(node)
+ return false unless node.is_a?(YARP::CallNode)
+ return false unless node.block
+
+ node.message == "type_member" || node.message == "type_template"
+ end
end
- class << self
+ class SigBuilder < Visitor
extend T::Sig
- sig { params(node: AST::Node).returns(Sig) }
- def build(node)
- v = SigBuilder.new
- v.visit_all(node.children)
- v.current
- end
- end
+ sig { returns(Sig) }
+ attr_accessor :current
- sig { override.params(node: T.nilable(AST::Node)).void }
- def visit(node)
- return unless node
+ sig { params(content: String, file: String).void }
+ def initialize(content, file:)
+ super
- case node.type
- when :send
- visit_send(node)
+ @current = T.let(Sig.new, Sig)
end
- end
- sig { params(node: AST::Node).void }
- def visit_send(node)
- visit(node.children[0]) if node.children[0]
- name = node.children[1]
- case name
- when :sig
- arg = node.children[2]
- @current.is_final = arg && arg.children[0] == :final
- when :abstract
- @current.is_abstract = true
- when :override
- @current.is_override = true
- when :overridable
- @current.is_overridable = true
- when :checked
- if node.children.length >= 3
- @current.checked = node.children[2].children[0]
- end
- when :type_parameters
- node.children[2..-1].each do |child|
- @current.type_params << child.children[0].to_s
- end
- when :params
- if node.children.length >= 3
- node.children[2].children.each do |child|
- name = child.children[0].children[0].to_s
- type = parse_expr(child.children[1])
- @current << SigParam.new(name, type)
+ sig { override.params(node: YARP::CallNode).void }
+ def visit_call_node(node)
+ case node.message
+ when "sig"
+ args = node.arguments
+ if args.is_a?(YARP::ArgumentsNode)
+ args.arguments.each do |arg|
+ @current.is_final = node_string(arg) == ":final"
+ end
end
+ when "abstract"
+ @current.is_abstract = true
+ when "checked"
+ args = node.arguments
+ if args.is_a?(YARP::ArgumentsNode)
+ arg = node_string(args.arguments.first)
+ @current.checked = arg&.delete_prefix(":")&.to_sym
+ end
+ when "override"
+ @current.is_override = true
+ when "overridable"
+ @current.is_overridable = true
+ when "params"
+ visit(node.arguments)
+ when "returns"
+ args = node.arguments
+ if args.is_a?(YARP::ArgumentsNode)
+ first = args.arguments.first
+ @current.return_type = node_string!(first) if first
+ end
+ when "type_parameters"
+ args = node.arguments
+ if args.is_a?(YARP::ArgumentsNode)
+ args.arguments.each do |arg|
+ @current.type_params << node_string!(arg).delete_prefix(":")
+ end
+ end
+ when "void"
+ @current.return_type = nil
end
- when :returns
- if node.children.length >= 3
- @current.return_type = parse_expr(node.children[2])
- end
- when :void
- @current.return_type = nil
- else
- raise "#{node.location.line}: Unhandled #{name}"
+
+ visit(node.receiver)
+ visit(node.block)
end
- end
- end
- class Loc
- class << self
- extend T::Sig
-
- sig { params(file: String, ast_loc: T.any(::Parser::Source::Map, ::Parser::Source::Range)).returns(Loc) }
- def from_ast_loc(file, ast_loc)
- Loc.new(
- file: file,
- begin_line: ast_loc.line,
- begin_column: ast_loc.column,
- end_line: ast_loc.last_line,
- end_column: ast_loc.last_column,
+ sig { override.params(node: YARP::AssocNode).void }
+ def visit_assoc_node(node)
+ @current.params << SigParam.new(
+ node_string!(node.key).delete_suffix(":"),
+ node_string!(T.must(node.value)),
)
end
end
end
end