lib/rbs/prototype/runtime/value_object_generator.rb



# frozen_string_literal: true

require_relative 'helpers'

module RBS
  module Prototype
    class Runtime
      class ValueObjectBase
        include Helpers

        def initialize(target_class)
          @target_class = target_class
        end

        def build_decl
          decl = AST::Declarations::Class.new(
            name: to_type_name(only_name(@target_class)),
            type_params: [],
            super_class: build_super_class,
            members: [],
            annotations: [],
            location: nil,
            comment: nil
          )

          add_decl_members(decl)

          decl
        end

        private

        # def self.members: () -> [ :foo, :bar ]
        # def members: () -> [ :foo, :bar ]
        def build_s_members
          (
            [:singleton, :instance] #: Array[AST::Members::MethodDefinition::kind]
          ).map do |kind|
            AST::Members::MethodDefinition.new(
              name: :members,
              overloads: [
                AST::Members::MethodDefinition::Overload.new(
                  annotations: [],
                  method_type: MethodType.new(
                    type: Types::Function.empty(
                      Types::Tuple.new(
                        types: @target_class.members.map do |member|
                          if member.to_s.ascii_only?
                            Types::Literal.new(literal: member, location: nil)
                          else
                            BuiltinNames::Symbol.instance_type
                          end
                        end,
                        location: nil
                      )
                    ),
                    type_params: [],
                    block: nil,
                    location: nil,
                  )
                )
              ],
              kind: kind,
              location: nil,
              comment: nil,
              annotations: [],
              overloading: false,
              visibility: nil
            )
          end
        end

        # attr_accessor foo: untyped
        def build_member_accessors(ast_members_class)
          @target_class.members.map do |member|
            ast_members_class.new(
              name: member,
              ivar_name: nil,
              type: untyped,
              kind: :instance,
              location: nil,
              comment: nil,
              annotations: (
                [] #: Array[AST::Annotation]
              )
            )
          end
        end
      end

      class StructGenerator < ValueObjectBase
        def self.generatable?(target)
          return false unless target < Struct
          # Avoid direct inherited class like `class Option < Struct`
          return false unless target.respond_to?(:members)

          true
        end

        private

        CAN_CALL_KEYWORD_INIT_P = Struct.new(:tmp).respond_to?(:keyword_init?)

        def build_super_class
          AST::Declarations::Class::Super.new(name: TypeName.parse("::Struct"), args: [untyped], location: nil)
        end

        def add_decl_members(decl)
          decl.members.concat build_s_new
          decl.members.concat build_s_keyword_init_p
          decl.members.concat build_s_members
          decl.members.concat build_member_accessors(AST::Members::AttrAccessor)
        end

        # def self.new: (?untyped foo, ?untyped bar) -> instance
        #             | (?foo: untyped, ?bar: untyped) -> instance
        def build_s_new
          [:new, :[]].map do |name|
            new_overloads = [] #: Array[AST::Members::MethodDefinition::Overload]

            if CAN_CALL_KEYWORD_INIT_P
              case @target_class.keyword_init?
              when false
                new_overloads << build_overload_for_positional_arguments
              when true
                new_overloads << build_overload_for_keyword_arguments
              when nil
                new_overloads << build_overload_for_positional_arguments
                new_overloads << build_overload_for_keyword_arguments
              else
                raise
              end
            else
              new_overloads << build_overload_for_positional_arguments
              new_overloads << build_overload_for_keyword_arguments
            end

            AST::Members::MethodDefinition.new(
              name: name,
              overloads: new_overloads,
              kind: :singleton,
              location: nil,
              comment: nil,
              annotations: [],
              overloading: false,
              visibility: nil
            )
          end
        end

        def build_overload_for_positional_arguments
          AST::Members::MethodDefinition::Overload.new(
            annotations: [],
            method_type: MethodType.new(
              type: Types::Function.empty(Types::Bases::Instance.new(location: nil)).update(
                optional_positionals: @target_class.members.map { |m| Types::Function::Param.new(name: m, type: untyped) },
              ),
              type_params: [],
              block: nil,
              location: nil,
            )
          )
        end

        def build_overload_for_keyword_arguments
          AST::Members::MethodDefinition::Overload.new(
            annotations: [],
            method_type: MethodType.new(
              type: Types::Function.empty(Types::Bases::Instance.new(location: nil)).update(
                optional_keywords: @target_class.members.to_h { |m| [m, Types::Function::Param.new(name: nil, type: untyped)] },
              ),
              type_params: [],
              block: nil,
              location: nil,
            )
          )
        end

        # def self.keyword_init?: () -> bool?
        def build_s_keyword_init_p
          return [] unless CAN_CALL_KEYWORD_INIT_P

          return_type = @target_class.keyword_init?.nil? \
                      ? Types::Bases::Nil.new(location: nil)
                      : Types::Literal.new(literal: @target_class.keyword_init?, location: nil)
          type = Types::Function.empty(return_type)

          [
            AST::Members::MethodDefinition.new(
              name: :keyword_init?,
              overloads: [
                AST::Members::MethodDefinition::Overload.new(
                  annotations: [],
                  method_type: MethodType.new(
                    type: type,
                    type_params: [],
                    block: nil,
                    location: nil,
                  )
                )
              ],
              kind: :singleton,
              location: nil,
              comment: nil,
              annotations: [],
              overloading: false,
              visibility: nil
            )
          ]
        end
      end

      class DataGenerator < ValueObjectBase
        def self.generatable?(target)
          return false unless RUBY_VERSION >= '3.2'
          return false unless target < Data
          # Avoid direct inherited class like `class Option < Data`
          return false unless target.respond_to?(:members)

          true
        end

        private

        def build_super_class
          AST::Declarations::Class::Super.new(name: TypeName.parse("::Data"), args: [], location: nil)
        end

        def add_decl_members(decl)
          decl.members.concat build_s_new
          decl.members.concat build_s_members
          decl.members.concat build_member_accessors(AST::Members::AttrReader)
        end

        # def self.new: (untyped foo, untyped bar) -> instance
        #             | (foo: untyped, bar: untyped) -> instance
        def build_s_new
          [:new, :[]].map do |name|
            new_overloads = [] #: Array[AST::Members::MethodDefinition::Overload]

            new_overloads << AST::Members::MethodDefinition::Overload.new(
              annotations: [],
              method_type: MethodType.new(
                type: Types::Function.empty(Types::Bases::Instance.new(location: nil)).update(
                  required_positionals: @target_class.members.map { |m| Types::Function::Param.new(name: m, type: untyped) },
                ),
                type_params: [],
                block: nil,
                location: nil,
              )
            )
            new_overloads << AST::Members::MethodDefinition::Overload.new(
              annotations: [],
              method_type: MethodType.new(
                type: Types::Function.empty(Types::Bases::Instance.new(location: nil)).update(
                  required_keywords: @target_class.members.to_h { |m| [m, Types::Function::Param.new(name: nil, type: untyped)] },
                ),
                type_params: [],
                block: nil,
                location: nil,
              )
            )

            AST::Members::MethodDefinition.new(
              name: name,
              overloads: new_overloads,
              kind: :singleton,
              location: nil,
              comment: nil,
              annotations: [],
              overloading: false,
              visibility: nil
            )
          end
        end
      end
    end
  end
end