# frozen_string_literal: true require "prism" module RbsInlineData class Parser < Prism::Visitor # @rbs skip TypedDefinition = Data.define( :class_name, #:: String :fields #:: Array[RbsInlineData::Parser::TypedField] ) # @rbs skip TypedField = Data.define( :field_name, #:: String :type #:: String ) # @rbs skip Comments = Data.define( :comment_lines #:: Hash[Integer, String] ) class Comments MARKER = "#::" #:: (Array[Prism::Comment]) -> RbsInlineData::Parser::Comments def self.from_prism_comments(comments) # @type var comment_lines: Hash[Integer, String] comment_lines = {} comments.each do |comment| sliced = comment.slice next unless sliced.start_with?(MARKER) comment_lines[comment.location.start_line] = sliced.sub(MARKER, "").strip end new(comment_lines:) end end # @rbs @definitions: Array[RbsInlineData::Parser::TypedDefinition] # @rbs @surronding_class_or_module: Array[Symbol] # @rbs @comments: RbsInlineData::Parser::Comments # rubocop:disable Lint/MissingSuper #:: (Array[RbsInlineData::Parser::TypedDefinition], RbsInlineData::Parser::Comments) -> void def initialize(definitions, comments) @definitions = definitions @comments = comments @surronding_class_or_module = [] end # rubocop:enable Lint/MissingSuper #:: (Prism::ParseResult) -> Array[RbsInlineData::Parser::TypedDefinition] def self.parse(result) # @type var definitions: Array[RbsInlineData::Parser::TypedDefinition] definitions = [] comments = Comments.from_prism_comments(result.comments) instance = new(definitions, comments) result.value.accept(instance) definitions end # @rbs override def visit_class_node(node) record_surrounding_class_or_module(node) { super } end # @rbs override def visit_module_node(node) record_surrounding_class_or_module(node) { super } end # @rbs override def visit_constant_write_node(node) if define_data?(node) definition = extract_definition(node) @definitions << definition if definition end super end private #:: (Prism::ClassNode | Prism::ModuleNode) { (Prism::ClassNode | Prism::ModuleNode) -> void } -> void def record_surrounding_class_or_module(node) @surronding_class_or_module.push(node.constant_path.name) yield(node) ensure @surronding_class_or_module.pop end #:: (Prism::ConstantWriteNode) -> bool def define_data?(node) node in { value: Prism::CallNode[ receiver: ( Prism::ConstantReadNode[name: :Data] | Prism::ConstantPathNode[parent: nil, name: :Data] ), name: :define, ] } end #:: (Prism::ConstantWriteNode) -> RbsInlineData::Parser::TypedDefinition? def extract_definition(node) arguments_node = node.value.arguments if arguments_node typed_fields = arguments_node.arguments.map do |sym_node| return nil unless sym_node.is_a?(Prism::SymbolNode) TypedField.new( field_name: sym_node.unescaped, type: type_of(sym_node) ) end.compact end TypedDefinition.new( class_name: "#{@surronding_class_or_module.join("::")}::#{node.name}", fields: typed_fields || [] ) end #:: (Prism::SymbolNode) -> String def type_of(node) @comments.comment_lines[node.location.start_line] || "untyped" end end end