lib/rbi/parser.rb in rbi-0.0.2 vs lib/rbi/parser.rb in rbi-0.0.3

- old
+ new

@@ -2,15 +2,26 @@ # frozen_string_literal: true require "unparser" module RBI - class Parser + class ParseError < StandardError extend T::Sig - class Error < StandardError; end + sig { returns(Loc) } + attr_reader :location + sig { params(message: String, location: Loc).void } + def initialize(message, location) + super(message) + @location = location + end + 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 @@ -27,31 +38,30 @@ end sig { params(string: String).returns(Tree) } def parse_string(string) parse(string, file: "-") - rescue ::Parser::SyntaxError => e - raise Error, e.message end sig { params(path: String).returns(Tree) } def parse_file(path) parse(::File.read(path), file: path) - rescue ::Parser::SyntaxError => e - raise Error, e.message 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: assoc) + builder.separate_header_comments builder.visit(node) builder.assoc_dangling_comments(comments) builder.tree + rescue ::Parser::SyntaxError => e + raise ParseError.new(e.message, Loc.from_ast_loc(file, e.diagnostic.location)) end end class ASTVisitor extend T::Helpers @@ -68,16 +78,16 @@ def visit(node); end private sig { params(node: AST::Node).returns(String) } - def visit_name(node) + def parse_name(node) T.must(ConstBuilder.visit(node)) end sig { params(node: AST::Node).returns(String) } - def visit_expr(node) + def parse_expr(node) Unparser.unparse(node) end end class TreeBuilder < ASTVisitor @@ -87,14 +97,14 @@ attr_reader :tree sig do params( file: String, - comments: T.nilable(T::Hash[::Parser::Source::Map, T::Array[::Parser::Source::Comment]]) + comments: T::Hash[::Parser::Source::Map, T::Array[::Parser::Source::Comment]] ).void end - def initialize(file:, comments: nil) + def initialize(file:, comments: {}) super() @file = file @comments = comments @tree = T.let(Tree.new, Tree) @scopes_stack = T.let([@tree], T::Array[Tree]) @@ -104,238 +114,328 @@ sig { override.params(node: T.nilable(Object)).void } def visit(node) return unless node.is_a?(AST::Node) case node.type when :module, :class, :sclass - visit_scope(node) + scope = parse_scope(node) + current_scope << scope + @scopes_stack << scope + visit_all(node.children) + @scopes_stack.pop when :casgn - visit_const_assign(node) + current_scope << parse_const_assign(node) when :def, :defs - visit_def(node) + current_scope << parse_def(node) when :send - visit_send(node) + node = parse_send(node) + current_scope << node if node when :block - visit_block(node) + node = parse_block(node) + if node.is_a?(Sig) + @last_sigs << node + elsif node + current_scope << node + end else visit_all(node.children) end end + sig { void } + def separate_header_comments + return if @comments.empty? + + keep = [] + node = T.must(@comments.keys.first) + comments = T.must(@comments.values.first) + + last_line = T.let(nil, T.nilable(Integer)) + comments.reverse.each do |comment| + comment_line = comment.location.last_line + + break if last_line && comment_line < last_line - 1 || + !last_line && comment_line < node.first_line - 1 + + keep << comment + last_line = comment_line + end + + @comments[node] = keep.reverse + end + sig { params(comments: T::Array[::Parser::Source::Comment]).void } def assoc_dangling_comments(comments) - return unless tree.empty? - comments.each do |comment| + last_line = T.let(nil, T.nilable(Integer)) + (comments - @comments.values.flatten).each do |comment| + comment_line = comment.location.last_line text = comment.text[1..-1].strip - loc = ast_to_rbi_loc(comment.location) + loc = Loc.from_ast_loc(@file, comment.location) + + if last_line && comment_line > last_line + 1 + # Preserve empty lines in file headers + tree.comments << EmptyComment.new(loc: loc) + end + tree.comments << Comment.new(text, loc: loc) + last_line = comment_line end end private - sig { params(node: AST::Node).void } - def visit_scope(node) + sig { params(node: AST::Node).returns(Scope) } + def parse_scope(node) loc = node_loc(node) comments = node_comments(node) - scope = case node.type + case node.type when :module - name = visit_name(node.children[0]) + name = parse_name(node.children[0]) Module.new(name, loc: loc, comments: comments) when :class - name = visit_name(node.children[0]) + 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 "Unsupported node #{node.type}" + raise ParseError.new("Unsupported scope node type `#{node.type}`", loc) end - current_scope << scope + end - @scopes_stack << scope - visit_all(node.children) - @scopes_stack.pop + 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) + 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) + end end - sig { params(node: AST::Node).void } - def visit_const_assign(node) - name = visit_name(node) - value = visit_expr(node.children[2]) + sig { params(node: AST::Node).returns(Method) } + def parse_def(node) loc = node_loc(node) - comments = node_comments(node) - current_scope << Const.new(name, value, loc: loc, comments: comments) - end - - sig { params(node: AST::Node).void } - def visit_def(node) - current_scope << case node.type + case node.type when :def Method.new( node.children[0].to_s, - params: node.children[1].children.map { |child| visit_param(child) }, + params: node.children[1].children.map { |child| parse_param(child) }, sigs: current_sigs, - loc: node_loc(node), + loc: loc, comments: node_comments(node) ) when :defs Method.new( node.children[1].to_s, - params: node.children[2].children.map { |child| visit_param(child) }, + params: node.children[2].children.map { |child| parse_param(child) }, is_singleton: true, sigs: current_sigs, - loc: node_loc(node), + loc: loc, comments: node_comments(node) ) else - raise "Unsupported node #{node.type}" + raise ParseError.new("Unsupported def node type `#{node.type}`", loc) end end sig { params(node: AST::Node).returns(Param) } - def visit_param(node) + def parse_param(node) name = node.children[0].to_s loc = node_loc(node) comments = node_comments(node) case node.type when :arg - Param.new(name, loc: loc, comments: comments) + ReqParam.new(name, loc: loc, comments: comments) when :optarg - value = visit_expr(node.children[1]) + 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 = visit_expr(node.children[1]) + 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 "Unsupported node #{node.type}" + raise ParseError.new("Unsupported param node type `#{node.type}`", loc) end end - sig { params(node: AST::Node).void } - def visit_send(node) + sig { params(node: AST::Node).returns(T.nilable(RBI::Node)) } + def parse_send(node) recv = node.children[0] - return if recv && recv != :self + return nil if recv && recv != :self method_name = node.children[1] loc = node_loc(node) comments = node_comments(node) - current_scope << case method_name + 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: comments) when :attr_writer symbols = node.children[2..-1].map { |child| child.children[0] } AttrWriter.new(*symbols, sigs: current_sigs, loc: loc, comments: comments) when :attr_accessor symbols = node.children[2..-1].map { |child| child.children[0] } AttrAccessor.new(*symbols, sigs: current_sigs, loc: loc, comments: comments) when :include - names = node.children[2..-1].map { |child| visit_name(child) } + names = node.children[2..-1].map { |child| parse_name(child) } Include.new(*names, loc: loc, comments: comments) when :extend - names = node.children[2..-1].map { |child| visit_name(child) } + names = node.children[2..-1].map { |child| parse_name(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| visit_name(child) } + names = node.children[2..-1].map { |child| parse_name(child) } MixesInClassMethods.new(*names, loc: loc, comments: comments) when :public, :protected, :private - Visibility.new(method_name, loc: loc) + visibility = Visibility.new(method_name, loc: loc) + 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) + 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 = visit_struct_prop(node) + 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 = visit_struct_prop(node) + name, type, default_value = parse_tstruct_prop(node) TStructConst.new(name, type, default: default_value, loc: loc, comments: comments) else - raise "Unsupported node #{node.type} with name #{method_name}" + raise ParseError.new("Unsupported send node with name `#{method_name}`", loc) end end - sig { params(node: AST::Node).void } - def visit_block(node) + sig { params(node: AST::Node).returns(T.nilable(RBI::Node)) } + def parse_block(node) name = node.children[0].children[1] case name when :sig - @last_sigs << visit_sig(node) + parse_sig(node) when :enums - current_scope << visit_enum(node) + parse_enum(node) else - raise "Unsupported node #{node.type} with name #{name}" + raise ParseError.new("Unsupported block node type `#{name}`", node_loc(node)) end end + 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 + + sig { params(node: AST::Node).returns(RBI::Struct) } + def parse_struct(node) + name = parse_name(node) + loc = node_loc(node) + comments = node_comments(node) + + 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] + 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 + end + 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 + + struct + end + sig { params(node: AST::Node).returns([String, String, T.nilable(String)]) } - def visit_struct_prop(node) + def parse_tstruct_prop(node) name = node.children[2].children[0].to_s - type = visit_expr(node.children[3]) + 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 - visit_expr(node.children.fetch(4, nil) + parse_expr(node.children.fetch(4, nil) &.children&.fetch(0, nil) &.children&.fetch(1, nil)) end [name, type, default_value] end sig { params(node: AST::Node).returns(Sig) } - def visit_sig(node) + def parse_sig(node) sig = SigBuilder.build(node) sig.loc = node_loc(node) sig end sig { params(node: AST::Node).returns(TEnumBlock) } - def visit_enum(node) + def parse_enum(node) enum = TEnumBlock.new node.children[2].children.each do |child| - enum << visit_name(child) + enum << parse_name(child) end enum.loc = node_loc(node) enum end sig { params(node: AST::Node).returns(Loc) } def node_loc(node) - ast_to_rbi_loc(node.location) + Loc.from_ast_loc(@file, node.location) end - sig { params(ast_loc: ::Parser::Source::Map).returns(Loc) } - def ast_to_rbi_loc(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 - ) - end - sig { params(node: AST::Node).returns(T::Array[Comment]) } def node_comments(node) - return [] unless @comments comments = @comments[node.location] return [] unless comments comments.map do |comment| text = comment.text[1..-1].strip - loc = ast_to_rbi_loc(comment.location) + loc = Loc.from_ast_loc(@file, comment.location) Comment.new(text, loc: loc) end end sig { returns(Tree) } @@ -432,18 +532,31 @@ @current.type_params << child.children[0].to_s end when :params node.children[2].children.each do |child| name = child.children[0].children[0].to_s - type = visit_expr(child.children[1]) + type = parse_expr(child.children[1]) @current << SigParam.new(name, type) end when :returns - @current.return_type = visit_expr(node.children[2]) + @current.return_type = parse_expr(node.children[2]) when :void @current.return_type = nil else raise "#{node.location.line}: Unhandled #{name}" end + end + end + + class Loc + sig { params(file: String, ast_loc: T.any(::Parser::Source::Map, ::Parser::Source::Range)).returns(Loc) } + def self.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 + ) end end end