module Steep class Source attr_reader :path attr_reader :node attr_reader :mapping extend NodeHelper def initialize(path:, node:, mapping:) @path = path @node = node @mapping = mapping end class Builder < ::Parser::Builders::Default def string_value(token) value(token) end self.emit_lambda = true self.emit_procarg0 = true self.emit_kwargs = true self.emit_forward_arg = true end def self.new_parser ::Parser::Ruby31.new(Builder.new).tap do |parser| parser.diagnostics.all_errors_are_fatal = true parser.diagnostics.ignore_warnings = true end end def self.parse(source_code, path:, factory:) buffer = ::Parser::Source::Buffer.new(path.to_s, 1, source: source_code) node, comments = new_parser().parse_with_comments(buffer) # @type var annotations: Array[AST::Annotation::t] annotations = [] # @type var type_comments: Hash[Integer, type_comment] type_comments = {} buffer = RBS::Buffer.new(name: path, content: source_code) annotation_parser = AnnotationParser.new(factory: factory) comments.each do |comment| if comment.inline? content = comment.text.delete_prefix('#') content.lstrip! prefix = comment.text.size - content.size content.rstrip! suffix = comment.text.size - content.size - prefix location = RBS::Location.new( buffer: buffer, start_pos: comment.location.expression.begin_pos + prefix, end_pos: comment.location.expression.end_pos - suffix ) case when annotation = annotation_parser.parse(content, location: location) annotations << annotation when assertion = AST::Node::TypeAssertion.parse(location) type_comments[assertion.line] = assertion when tapp = AST::Node::TypeApplication.parse(location) type_comments[tapp.line] = tapp end end end map = {} map.compare_by_identity if node node = insert_type_node(node, type_comments) construct_mapping(node: node, annotations: annotations, mapping: map) end annotations.each do |annot| map[node] ||= [] map[node] << annot end new(path: path, node: node, mapping: map) end def self.construct_mapping(node:, annotations:, mapping:, line_range: nil) case node.type when :if if node.loc.respond_to?(:question) # Skip ternary operator each_child_node node do |child| construct_mapping(node: child, annotations: annotations, mapping: mapping, line_range: nil) end else if node.loc.expression.begin_pos == node.loc.keyword.begin_pos construct_mapping(node: node.children[0], annotations: annotations, mapping: mapping, line_range: nil) if node.children[1] if node.loc.keyword.source == "if" || node.loc.keyword.source == "elsif" then_start = node.loc.begin&.last_line || node.children[0].loc.last_line then_end = node.children[2] ? node.loc.else.line : node.loc.last_line else then_start = node.loc.else.last_line then_end = node.loc.last_line end construct_mapping(node: node.children[1], annotations: annotations, mapping: mapping, line_range: then_start...then_end) end if node.children[2] if node.loc.keyword.source == "if" || node.loc.keyword.source == "elsif" else_start = node.loc.else.last_line else_end = node.loc.last_line else else_start = node.loc.begin&.last_line || node.children[0].loc.last_line else_end = node.children[1] ? node.loc.else.line : node.loc.last_line end construct_mapping(node: node.children[2], annotations: annotations, mapping: mapping, line_range: else_start...else_end) end else # postfix if/unless each_child_node(node) do |child| construct_mapping(node: child, annotations: annotations, mapping: mapping, line_range: nil) end end end when :while, :until if node.loc.expression.begin_pos == node.loc.keyword.begin_pos construct_mapping(node: node.children[0], annotations: annotations, mapping: mapping, line_range: nil) if node.children[1] body_start = node.children[0].loc.last_line body_end = node.loc.end.line construct_mapping(node: node.children[1], annotations: annotations, mapping: mapping, line_range: body_start...body_end) end else # postfix while each_child_node(node) do |child| construct_mapping(node: child, annotations: annotations, mapping: mapping, line_range: nil) end end when :while_post, :until_post construct_mapping(node: node.children[0], annotations: annotations, mapping: mapping, line_range: nil) if node.children[1] body_start = node.loc.expression.line body_end = node.loc.keyword.line construct_mapping(node: node.children[1], annotations: annotations, mapping: mapping, line_range: body_start...body_end) end when :case if node.children[0] construct_mapping(node: node.children[0], annotations: annotations, mapping: mapping, line_range: nil) end if node.children.last else_node = node.children.last else_start = node.loc.else.last_line else_end = node.loc.end.line construct_mapping(node: else_node, annotations: annotations, mapping: mapping, line_range: else_start...else_end) end node.children.drop(1).each do |child| if child&.type == :when construct_mapping(node: child, annotations: annotations, mapping: mapping, line_range: nil) end end when :when last_cond = node.children[-2] body = node.children.last node.children.take(node.children.size-1).each do |child| construct_mapping(node: child, annotations: annotations, mapping: mapping, line_range: nil) end if body cond_end = last_cond.loc.last_line+1 body_end = body.loc.last_line construct_mapping(node: body, annotations: annotations, mapping: mapping, line_range: cond_end...body_end) end when :rescue if node.children.last else_node = node.children.last else_start = node.loc.else.last_line else_end = node.loc.last_line construct_mapping(node: else_node, annotations: annotations, mapping: mapping, line_range: else_start...else_end) end each_child_node(node) do |child| construct_mapping(node: child, annotations: annotations, mapping: mapping, line_range: nil) end else each_child_node(node) do |child| construct_mapping(node: child, annotations: annotations, mapping: mapping, line_range: nil) end end associated_annotations, other_annotations = annotations.partition do |annot| case node.type when :def, :module, :class, :block, :ensure, :defs loc = node.loc loc.line <= annot.line && annot.line < loc.last_line when :resbody if node.loc.keyword.begin_pos == node.loc.expression.begin_pos # skip postfix rescue loc = node.loc loc.line <= annot.line && annot.line < loc.last_line end else if line_range line_range.begin <= annot.line && annot.line < line_range.end end end end associated_annotations.each do |annot| mapping[node] ||= [] mapping[node] << annot end annotations.replace(other_annotations) end def self.map_child_node(node, type = nil, skip: nil) children = node.children.map do |child| if child.is_a?(Parser::AST::Node) if skip if skip.member?(child) child else yield child end else yield child end else child end end node.updated(type, children) end def annotations(block:, factory:, context:) AST::Annotation::Collection.new( annotations: (mapping[block] || []), factory: factory, context: context ) end def each_annotation(&block) if block_given? mapping.each do |node, annots| yield [node, annots] end else enum_for :each_annotation end end def each_heredoc_node(node = self.node, parents = [], &block) if block return unless node case node.type when :dstr, :str if node.location.respond_to?(:heredoc_body) yield [node, *parents] end end parents.unshift(node) Source.each_child_node(node) do |child| each_heredoc_node(child, parents, &block) end parents.shift() else enum_for :each_heredoc_node, node end end def find_heredoc_nodes(line, column, position) each_heredoc_node() do |nodes| node = nodes[0] range = node.location.heredoc_body&.yield_self do |r| r.begin_pos..r.end_pos end if range && (range === position) return nodes end end nil end def find_nodes_loc(node, position, parents) range = node.location.expression&.yield_self do |r| r.begin_pos..r.end_pos end if range if range === position parents.unshift node Source.each_child_node(node) do |child| if ns = find_nodes_loc(child, position, parents) return ns end end parents end end end def find_nodes(line:, column:) return [] unless node position = (line-1).times.sum do |i| node.location.expression.source_buffer.source_line(i+1).size + 1 end + column if heredoc_nodes = find_heredoc_nodes(line, column, position) Source.each_child_node(heredoc_nodes[0]) do |child| if nodes = find_nodes_loc(child, position, heredoc_nodes) return nodes end end return heredoc_nodes else find_nodes_loc(node, position, []) end end def self.delete_defs(node, allow_list) case node.type when :def if allow_list.include?(node) node else node.updated(:nil, []) end when :defs if allow_list.include?(node) node else delete_defs(node.children[0], allow_list) end else map_child_node(node) do |child| delete_defs(child, allow_list) end end end def without_unrelated_defs(line:, column:) if node nodes = find_nodes(line: line, column: column) || [] defs = Set[].compare_by_identity.merge(nodes.select {|node| node.type == :def || node.type == :defs }) node_ = Source.delete_defs(node, defs) # @type var mapping: Hash[Parser::AST::Node, Array[AST::Annotation::t]] mapping = {} mapping.compare_by_identity annotations = self.mapping.values.flatten Source.construct_mapping(node: node_, annotations: annotations, mapping: mapping) annotations.each do |annot| mapping[node_] ||= [] mapping[node_] << annot end Source.new(path: path, node: node_, mapping: mapping) else self end end def self.insert_type_node(node, comments) if node.location.expression first_line = node.location.expression.first_line last_line = node.location.expression.last_line last_comment = comments[last_line] if (first_line..last_line).none? {|l| comments.key?(l) } return node end case when last_comment.is_a?(AST::Node::TypeAssertion) case node.type when :lvasgn, :ivasgn, :gvasgn, :cvasgn, :casgn # Skip when :masgn lhs, rhs = node.children node = node.updated(nil, [lhs, insert_type_node(rhs, comments)]) return adjust_location(node) when :return, :break, :next # Skip when :begin if node.loc.begin # paren child_assertions = comments.except(last_line) node = map_child_node(node) {|child| insert_type_node(child, child_assertions) } node = adjust_location(node) return assertion_node(node, last_comment) end else child_assertions = comments.except(last_line) node = map_child_node(node) {|child| insert_type_node(child, child_assertions) } node = adjust_location(node) return assertion_node(node, last_comment) end when selector_line = sendish_node?(node) if (comment = comments[selector_line]).is_a?(AST::Node::TypeApplication) child_assertions = comments.except(selector_line) case node.type when :block send, *children = node.children node = node.updated( nil, [ map_child_node(send) {|child| insert_type_node(child, child_assertions) }, *children.map {|child| insert_type_node(child, child_assertions) } ] ) when :numblock send, size, body = node.children node = node.updated( nil, [ map_child_node(send) {|child| insert_type_node(child, child_assertions) }, size, insert_type_node(body, child_assertions) ] ) else node = map_child_node(node) {|child| insert_type_node(child, child_assertions) } end node = adjust_location(node) return type_application_node(node, comment) end end end adjust_location( map_child_node(node, nil) {|child| insert_type_node(child, comments) } ) end def self.sendish_node?(node) send_node = case node.type when :send, :csend node when :block, :numblock send = node.children[0] case send.type when :send, :csend send end end if send_node if send_node.location.dot send_node.location.selector.line end end end def self.adjust_location(node) if end_pos = node.location.expression&.end_pos if last_pos = each_child_node(node).map {|node| node.location.expression&.end_pos }.compact.max if last_pos > end_pos props = { location: node.location.with_expression(node.location.expression.with(end_pos: last_pos)) } end end end if props node.updated(nil, nil, props) else node end end def self.assertion_node(node, type) map = Parser::Source::Map.new(node.location.expression.with(end_pos: type.location.end_pos)) Parser::AST::Node.new(:assertion, [node, type], { location: map }) end def self.type_application_node(node, tapp) if node.location.expression.end_pos > tapp.location.end_pos map = Parser::Source::Map.new(node.location.expression) else map = Parser::Source::Map.new(node.location.expression.with(end_pos: tapp.location.end_pos)) end node = Parser::AST::Node.new(:tapp, [node, tapp], { location: map }) tapp.set_node(node) node end end end