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