lib/solargraph/rbs_map/conversions.rb



# frozen_string_literal: true


require 'rbs'

module Solargraph
  class RbsMap
    # Functions for converting RBS declarations to Solargraph pins

    #

    module Conversions
      # A container for tracking the current context of the RBS conversion

      # process, e.g., what visibility is declared for methods in the current

      # scope

      #

      class Context
        attr_reader :visibility

        # @param visibility [Symbol]

        def initialize visibility = :public
          @visibility = visibility
        end
      end

      # @return [Array<Pin::Base>]

      def pins
        @pins ||= []
      end

      private

      # @return [Hash{String => RBS::AST::Declarations::TypeAlias}]

      def type_aliases
        @type_aliases ||= {}
      end

      # @param loader [RBS::EnvironmentLoader]

      # @return [void]

      def load_environment_to_pins(loader)
        environment = RBS::Environment.from_loader(loader).resolve_type_names
        cursor = pins.length
        environment.declarations.each { |decl| convert_decl_to_pin(decl, Solargraph::Pin::ROOT_PIN) }
        added_pins = pins[cursor..-1]
        added_pins.each { |pin| pin.source = :rbs }
      end

      # @param decl [RBS::AST::Declarations::Base]

      # @param closure [Pin::Closure]

      # @return [void]

      def convert_decl_to_pin decl, closure
        case decl
        when RBS::AST::Declarations::Class
          class_decl_to_pin decl
        when RBS::AST::Declarations::Interface
          # STDERR.puts "Skipping interface #{decl.name.relative!}"

          interface_decl_to_pin decl, closure
        when RBS::AST::Declarations::TypeAlias
          type_aliases[decl.name.to_s] = decl
        when RBS::AST::Declarations::Module
          module_decl_to_pin decl
        when RBS::AST::Declarations::Constant
          constant_decl_to_pin decl
        when RBS::AST::Declarations::ClassAlias
          class_alias_decl_to_pin decl
        when RBS::AST::Declarations::ModuleAlias
          module_alias_decl_to_pin decl
        when RBS::AST::Declarations::Global
          global_decl_to_pin decl
        else
          Solargraph.logger.warn "Skipping declaration #{decl.class}"
        end
      end

      # @param decl [RBS::AST::Declarations::Module]

      # @param module_pin [Pin::Namespace]

      # @return [void]

      def convert_self_types_to_pins decl, module_pin
        decl.self_types.each { |self_type| context = convert_self_type_to_pins(self_type, module_pin) }
      end

      # @param decl [RBS::AST::Declarations::Module::Self]

      # @param closure [Pin::Namespace]

      # @return [void]

      def convert_self_type_to_pins decl, closure
        type = build_type(decl.name, decl.args)
        generic_values = type.all_params.map(&:to_s)
        include_pin = Solargraph::Pin::Reference::Include.new(
          name: decl.name.relative!.to_s,
          type_location: location_decl_to_pin_location(decl.location),
          generic_values: generic_values,
          closure: closure
        )
        pins.push include_pin
      end

      # @param decl [RBS::AST::Declarations::Module,RBS::AST::Declarations::Class,RBS::AST::Declarations::Interface]

      # @param closure [Pin::Namespace]

      # @return [void]

      def convert_members_to_pins decl, closure
        context = Context.new
        decl.members.each { |m| context = convert_member_to_pin(m, closure, context) }
      end

      # @param member [RBS::AST::Members::Base,RBS::AST::Declarations::Base]

      # @param closure [Pin::Namespace]

      # @param context [Context]

      # @return [void]

      def convert_member_to_pin member, closure, context
        case member
        when RBS::AST::Members::MethodDefinition
          method_def_to_pin(member, closure)
        when RBS::AST::Members::AttrReader
          attr_reader_to_pin(member, closure)
        when RBS::AST::Members::AttrWriter
          attr_writer_to_pin(member, closure)
        when RBS::AST::Members::AttrAccessor
          attr_accessor_to_pin(member, closure)
        when RBS::AST::Members::Include
          include_to_pin(member, closure)
        when RBS::AST::Members::Prepend
          prepend_to_pin(member, closure)
        when RBS::AST::Members::Extend
          extend_to_pin(member, closure)
        when RBS::AST::Members::Alias
          alias_to_pin(member, closure)
        when RBS::AST::Members::ClassInstanceVariable
          civar_to_pin(member, closure)
        when RBS::AST::Members::ClassVariable
          cvar_to_pin(member, closure)
        when RBS::AST::Members::InstanceVariable
          ivar_to_pin(member, closure)
        when RBS::AST::Members::Public
          return Context.new(visibility: :public)
        when RBS::AST::Members::Private
          return Context.new(visibility: :private)
        when RBS::AST::Declarations::Base
          convert_decl_to_pin(member, closure)
        else
          Solargraph.logger.warn "Skipping member type #{member.class}"
        end
        context
      end

      # @param decl [RBS::AST::Declarations::Class]

      # @return [void]

      def class_decl_to_pin decl
        class_pin = Solargraph::Pin::Namespace.new(
          type: :class,
          name: decl.name.relative!.to_s,
          closure: Solargraph::Pin::ROOT_PIN,
          comments: decl.comment&.string,
          type_location: location_decl_to_pin_location(decl.location),
          # @todo some type parameters in core/stdlib have default

          #   values; Solargraph doesn't support that yet as so these

          #   get treated as undefined if not specified

          generics: decl.type_params.map(&:name).map(&:to_s)
        )
        pins.push class_pin
        if decl.super_class
          pins.push Solargraph::Pin::Reference::Superclass.new(
            type_location: location_decl_to_pin_location(decl.super_class.location),
            closure: class_pin,
            name: decl.super_class.name.relative!.to_s
          )
        end
        add_mixins decl, class_pin
        convert_members_to_pins decl, class_pin
      end

      # @param decl [RBS::AST::Declarations::Interface]

      # @param closure [Pin::Closure]

      # @return [void]

      def interface_decl_to_pin decl, closure
        class_pin = Solargraph::Pin::Namespace.new(
          type: :module,
          type_location: location_decl_to_pin_location(decl.location),
          name: decl.name.relative!.to_s,
          closure: Solargraph::Pin::ROOT_PIN,
          comments: decl.comment&.string,
          generics: decl.type_params.map(&:name).map(&:to_s),
          # HACK: Using :hidden to keep interfaces from appearing in

          # autocompletion

          visibility: :hidden
        )
        class_pin.docstring.add_tag(YARD::Tags::Tag.new(:abstract, '(RBS interface)'))
        pins.push class_pin
        convert_members_to_pins decl, class_pin
      end

      # @param decl [RBS::AST::Declarations::Module]

      # @return [void]

      def module_decl_to_pin decl
        module_pin = Solargraph::Pin::Namespace.new(
          type: :module,
          name: decl.name.relative!.to_s,
          type_location: location_decl_to_pin_location(decl.location),
          closure: Solargraph::Pin::ROOT_PIN,
          comments: decl.comment&.string,
          generics: decl.type_params.map(&:name).map(&:to_s),
        )
        pins.push module_pin
        convert_self_types_to_pins decl, module_pin
        convert_members_to_pins decl, module_pin

        add_mixins decl, module_pin.closure
      end

      # @param name [String]

      # @param tag [String]

      # @param comments [String]

      # @param decl [RBS::AST::Declarations::ClassAlias, RBS::AST::Declarations::Constant, RBS::AST::Declarations::ModuleAlias]

      # @param base [String, nil] Optional conversion of tag to base<tag>

      #

      # @return [Solargraph::Pin::Constant]

      def create_constant(name, tag, comments, decl, base = nil)
        parts = name.split('::')
        if parts.length > 1
          name = parts.last
          closure = pins.select { |pin| pin && pin.path == parts[0..-2].join('::') }.first
        else
          name = parts.first
          closure = Solargraph::Pin::ROOT_PIN
        end
        constant_pin = Solargraph::Pin::Constant.new(
          name: name,
          closure: closure,
          type_location: location_decl_to_pin_location(decl.location),
          comments: comments
        )
        tag = "#{base}<#{tag}>" if base
        rooted_tag = ComplexType.parse(tag).force_rooted.rooted_tags
        constant_pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag))
        constant_pin
      end

      # @param decl [RBS::AST::Declarations::ClassAlias]

      # @return [void]

      def class_alias_decl_to_pin decl
        # See https://www.rubydoc.info/gems/rbs/3.4.3/RBS/AST/Declarations/ClassAlias

        new_name = decl.new_name.relative!.to_s
        old_name = decl.old_name.relative!.to_s

        pins.push create_constant(new_name, old_name, decl.comment&.string, decl, 'Class')
      end

      # @param decl [RBS::AST::Declarations::ModuleAlias]

      # @return [void]

      def module_alias_decl_to_pin decl
        # See https://www.rubydoc.info/gems/rbs/3.4.3/RBS/AST/Declarations/ModuleAlias

        new_name = decl.new_name.relative!.to_s
        old_name = decl.old_name.relative!.to_s

        pins.push create_constant(new_name, old_name, decl.comment&.string, decl,  'Module')
      end

      # @param decl [RBS::AST::Declarations::Constant]

      # @return [void]

      def constant_decl_to_pin decl
        tag = other_type_to_tag(decl.type)
        pins.push create_constant(decl.name.relative!.to_s, tag, decl.comment&.string, decl)
      end

      # @param decl [RBS::AST::Declarations::Global]

      # @return [void]

      def global_decl_to_pin decl
        closure = Solargraph::Pin::ROOT_PIN
        name = decl.name.to_s
        pin = Solargraph::Pin::GlobalVariable.new(
          name: name,
          closure: closure,
          comments: decl.comment&.string,
        )
        rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags
        pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag))
        pins.push pin
      end

      # @param decl [RBS::AST::Members::MethodDefinition]

      # @param closure [Pin::Closure]

      # @return [void]

      def method_def_to_pin decl, closure
        # there may be edge cases here around different signatures

        # having different type params / orders - we may need to match

        # this data model and have generics live in signatures to

        # handle those correctly

        generics = decl.overloads.map(&:method_type).flat_map(&:type_params).map(&:name).map(&:to_s).uniq
        if decl.instance?
          pin = Solargraph::Pin::Method.new(
            name: decl.name.to_s,
            closure: closure,
            type_location: location_decl_to_pin_location(decl.location),
            comments: decl.comment&.string,
            scope: :instance,
            signatures: [],
            generics: generics,
            # @todo RBS core has unreliable visibility definitions

            visibility: closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(decl.name) ? :private : :public
          )
          pin.signatures.concat method_def_to_sigs(decl, pin)
          pins.push pin
          if pin.name == 'initialize'
            pin.instance_variable_set(:@visibility, :private)
            pin.instance_variable_set(:@return_type, ComplexType::VOID)
          end
        end
        if decl.singleton?
          pin = Solargraph::Pin::Method.new(
            name: decl.name.to_s,
            closure: closure,
            comments: decl.comment&.string,
            type_location: location_decl_to_pin_location(decl.location),
            scope: :class,
            signatures: [],
            generics: generics
          )
          pin.signatures.concat method_def_to_sigs(decl, pin)
          pins.push pin
        end
      end

      # @param decl [RBS::AST::Members::MethodDefinition]

      # @param pin [Pin::Method]

      # @return [void]

      def method_def_to_sigs decl, pin
        decl.overloads.map do |overload|
          generics = overload.method_type.type_params.map(&:to_s)
          signature_parameters, signature_return_type = parts_of_function(overload.method_type, pin)
          block = if overload.method_type.block
                    block_parameters, block_return_type = parts_of_function(overload.method_type.block, pin)
                    Pin::Signature.new(generics: generics, parameters: block_parameters, return_type: block_return_type)
                  end
          Pin::Signature.new(generics: generics, parameters: signature_parameters, return_type: signature_return_type, block: block)
        end
      end

      # @param location [RBS::Location, nil]

      # @return [Solargraph::Location, nil]

      def location_decl_to_pin_location(location)
        return nil if location&.name.nil?

        start_pos = Position.new(location.start_line - 1, location.start_column)
        end_pos = Position.new(location.end_line - 1, location.end_column)
        range = Range.new(start_pos, end_pos)
        Location.new(location.name.to_s, range)
      end

      # @param type [RBS::MethodType,RBS::Types::Block]

      # @param pin [Pin::Method]

      # @return [Array(Array<Pin::Parameter>, ComplexType)]

      def parts_of_function type, pin
        return [[Solargraph::Pin::Parameter.new(decl: :restarg, name: 'arg', closure: pin)], ComplexType.try_parse(method_type_to_tag(type)).force_rooted] if defined?(RBS::Types::UntypedFunction) && type.type.is_a?(RBS::Types::UntypedFunction)

        parameters = []
        arg_num = -1
        type.type.required_positionals.each do |param|
          name = param.name ? param.name.to_s : "arg#{arg_num += 1}"
          parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted)
        end
        type.type.optional_positionals.each do |param|
          name = param.name ? param.name.to_s : "arg#{arg_num += 1}"
          parameters.push Solargraph::Pin::Parameter.new(decl: :optarg, name: name, closure: pin,
                                                         return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted)
        end
        if type.type.rest_positionals
          name = type.type.rest_positionals.name ? type.type.rest_positionals.name.to_s : "arg#{arg_num += 1}"
          parameters.push Solargraph::Pin::Parameter.new(decl: :restarg, name: name, closure: pin)
        end
        type.type.trailing_positionals.each do |param|
          name = param.name ? param.name.to_s : "arg#{arg_num += 1}"
          parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin)
        end
        type.type.required_keywords.each do |orig, param|
          name = orig ? orig.to_s : "arg#{arg_num += 1}"
          parameters.push Solargraph::Pin::Parameter.new(decl: :kwarg, name: name, closure: pin,
                                                         return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted)
        end
        type.type.optional_keywords.each do |orig, param|
          name = orig ? orig.to_s : "arg#{arg_num += 1}"
          parameters.push Solargraph::Pin::Parameter.new(decl: :kwoptarg, name: name, closure: pin,
                                                         return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted)
        end
        if type.type.rest_keywords
          name = type.type.rest_keywords.name ? type.type.rest_keywords.name.to_s : "arg#{arg_num += 1}"
          parameters.push Solargraph::Pin::Parameter.new(decl: :kwrestarg, name: type.type.rest_keywords.name.to_s, closure: pin)
        end

        rooted_tag = method_type_to_tag(type)
        return_type = ComplexType.try_parse(rooted_tag).force_rooted
        [parameters, return_type]
      end

      # @param decl [RBS::AST::Members::AttrReader,RBS::AST::Members::AttrAccessor]

      # @param closure [Pin::Namespace]

      # @return [void]

      def attr_reader_to_pin(decl, closure)
        pin = Solargraph::Pin::Method.new(
          name: decl.name.to_s,
          type_location: location_decl_to_pin_location(decl.location),
          closure: closure,
          comments: decl.comment&.string,
          scope: :instance,
          attribute: true
        )
        rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags
        pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag))
        pins.push pin
      end

      # @param decl [RBS::AST::Members::AttrWriter, RBS::AST::Members::AttrAccessor]

      # @param closure [Pin::Namespace]

      # @return [void]

      def attr_writer_to_pin(decl, closure)
        pin = Solargraph::Pin::Method.new(
          name: "#{decl.name.to_s}=",
          type_location: location_decl_to_pin_location(decl.location),
          closure: closure,
          comments: decl.comment&.string,
          scope: :instance,
          attribute: true
        )
        rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags
        pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag))
        pins.push pin
      end

      # @param decl [RBS::AST::Members::AttrAccessor]

      # @param closure [Pin::Namespace]

      # @return [void]

      def attr_accessor_to_pin(decl, closure)
        attr_reader_to_pin(decl, closure)
        attr_writer_to_pin(decl, closure)
      end

      # @param decl [RBS::AST::Members::InstanceVariable]

      # @param closure [Pin::Namespace]

      # @return [void]

      def ivar_to_pin(decl, closure)
        pin = Solargraph::Pin::InstanceVariable.new(
          name: decl.name.to_s,
          closure: closure,
          type_location: location_decl_to_pin_location(decl.location),
          comments: decl.comment&.string
        )
        rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags
        pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag))
        pins.push pin
      end

      # @param decl [RBS::AST::Members::ClassVariable]

      # @param closure [Pin::Namespace]

      # @return [void]

      def cvar_to_pin(decl, closure)
        name = decl.name.to_s
        pin = Solargraph::Pin::ClassVariable.new(
          name: name,
          closure: closure,
          comments: decl.comment&.string
        )
        rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags
        pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag))
        pins.push pin
      end

      # @param decl [RBS::AST::Members::ClassInstanceVariable]

      # @param closure [Pin::Namespace]

      # @return [void]

      def civar_to_pin(decl, closure)
        name = decl.name.to_s
        pin = Solargraph::Pin::InstanceVariable.new(
          name: name,
          closure: closure,
          comments: decl.comment&.string
        )
        rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags
        pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag))
        pins.push pin
      end

      # @param decl [RBS::AST::Members::Include]

      # @param closure [Pin::Namespace]

      # @return [void]

      def include_to_pin decl, closure
        type = build_type(decl.name, decl.args)
        generic_values = type.all_params.map(&:to_s)
        pins.push Solargraph::Pin::Reference::Include.new(
          name: decl.name.relative!.to_s,
          type_location: location_decl_to_pin_location(decl.location),
          generic_values: generic_values,
          closure: closure
        )
      end

      # @param decl [RBS::AST::Members::Prepend]

      # @param closure [Pin::Namespace]

      # @return [void]

      def prepend_to_pin decl, closure
        pins.push Solargraph::Pin::Reference::Prepend.new(
          name: decl.name.relative!.to_s,
          type_location: location_decl_to_pin_location(decl.location),
          closure: closure
        )
      end

      # @param decl [RBS::AST::Members::Extend]

      # @param closure [Pin::Namespace]

      # @return [void]

      def extend_to_pin decl, closure
        pins.push Solargraph::Pin::Reference::Extend.new(
          name: decl.name.relative!.to_s,
          type_location: location_decl_to_pin_location(decl.location),
          closure: closure
        )
      end

      # @param decl [RBS::AST::Members::Alias]

      # @param closure [Pin::Namespace]

      # @return [void]

      def alias_to_pin decl, closure
        pins.push Solargraph::Pin::MethodAlias.new(
          name: decl.new_name.to_s,
          type_location: location_decl_to_pin_location(decl.location),
          original: decl.old_name.to_s,
          closure: closure
        )
      end

      RBS_TO_YARD_TYPE = {
        'bool' => 'Boolean',
        'string' => 'String',
        'int' => 'Integer',
        'untyped' => '',
        'NilClass' => 'nil'
      }

      # @param type [RBS::MethodType]

      # @return [String]

      def method_type_to_tag type
        if type_aliases.key?(type.type.return_type.to_s)
          other_type_to_tag(type_aliases[type.type.return_type.to_s].type)
        else
          other_type_to_tag type.type.return_type
        end
      end

      # @param type_name [RBS::TypeName]

      # @param type_args [Enumerable<RBS::Types::Bases::Base>]

      # @return [ComplexType::UniqueType]

      def build_type(type_name, type_args = [])
        base = RBS_TO_YARD_TYPE[type_name.relative!.to_s] || type_name.relative!.to_s
        params = type_args.map { |a| other_type_to_tag(a) }.reject { |t| t == 'undefined' }.map do |t|
          ComplexType.try_parse(t).force_rooted
        end
        if base == 'Hash' && params.length == 2
          ComplexType::UniqueType.new(base, [params.first], [params.last], rooted: true, parameters_type: :hash)
        else
          ComplexType::UniqueType.new(base, [], params, rooted: true, parameters_type: :list)
        end
      end

      # @param type_name [RBS::TypeName]

      # @param type_args [Enumerable<RBS::Types::Bases::Base>]

      # @return [String]

      def type_tag(type_name, type_args = [])
        build_type(type_name, type_args).tags
      end

      # @param type [RBS::Types::Bases::Base]

      # @return [String]

      def other_type_to_tag type
        if type.is_a?(RBS::Types::Optional)
          "#{other_type_to_tag(type.type)}, nil"
        elsif type.is_a?(RBS::Types::Bases::Any)
          # @todo Not sure what to do with Any yet

          'BasicObject'
        elsif type.is_a?(RBS::Types::Bases::Bool)
          'Boolean'
        elsif type.is_a?(RBS::Types::Tuple)
          "Array(#{type.types.map { |t| other_type_to_tag(t) }.join(', ')})"
        elsif type.is_a?(RBS::Types::Literal)
          type.literal.to_s
        elsif type.is_a?(RBS::Types::Union)
          type.types.map { |t| other_type_to_tag(t) }.join(', ')
        elsif type.is_a?(RBS::Types::Record)
          # @todo Better record support

          'Hash'
        elsif type.is_a?(RBS::Types::Bases::Nil)
          'nil'
        elsif type.is_a?(RBS::Types::Bases::Self)
          'self'
        elsif type.is_a?(RBS::Types::Bases::Void)
          'void'
        elsif type.is_a?(RBS::Types::Variable)
          "#{Solargraph::ComplexType::GENERIC_TAG_NAME}<#{type.name}>"
        elsif type.is_a?(RBS::Types::ClassInstance) #&& !type.args.empty?

          type_tag(type.name, type.args)
        elsif type.is_a?(RBS::Types::Bases::Instance)
          'self'
        elsif type.is_a?(RBS::Types::Bases::Top)
          # top is the most super superclass

          'BasicObject'
        elsif type.is_a?(RBS::Types::Bases::Bottom)
          # bottom is used in contexts where nothing will ever return

          # - e.g., it could be the return type of 'exit()' or 'raise'

          #

          # @todo define a specific bottom type and use it to

          #   determine dead code

          'undefined'
        elsif type.is_a?(RBS::Types::Intersection)
          type.types.map { |member| other_type_to_tag(member) }.join(', ')
        elsif type.is_a?(RBS::Types::Proc)
          'Proc'
        elsif type.is_a?(RBS::Types::Alias)
          # type-level alias use - e.g., 'bool' in "type bool = true | false"

          # @todo ensure these get resolved after processing all aliases

          # @todo handle recursive aliases

          type_tag(type.name, type.args)
        elsif type.is_a?(RBS::Types::Interface)
          # represents a mix-in module which can be considered a

          # subtype of a consumer of it

          type_tag(type.name, type.args)
        elsif type.is_a?(RBS::Types::ClassSingleton)
          # e.g., singleton(String)

          type_tag(type.name)
        else
          Solargraph.logger.warn "Unrecognized RBS type: #{type.class} at #{type.location}"
          'undefined'
        end
      end

      # @param decl [RBS::AST::Declarations::Class, RBS::AST::Declarations::Module]

      # @param namespace [Pin::Namespace]

      # @return [void]

      def add_mixins decl, namespace
        decl.each_mixin do |mixin|
          klass = mixin.is_a?(RBS::AST::Members::Include) ? Pin::Reference::Include : Pin::Reference::Extend
          type = build_type(mixin.name, mixin.args)
          generic_values = type.all_params.map(&:to_s)
          pins.push klass.new(
            name: mixin.name.relative!.to_s,
            type_location: location_decl_to_pin_location(mixin.location),
            generic_values: generic_values,
            closure: namespace
          )
        end
      end
    end
  end
end