lib/types/private/methods/decl_builder.rb



# frozen_string_literal: true
# typed: true

module T::Private::Methods
  Declaration = Struct.new(
    :mod,
    :params,
    :returns,
    :bind,
    :mode,
    :checked,
    :finalized,
    :on_failure,
    :override_allow_incompatible,
    :type_parameters,
    :raw,
    :final
  )

  class DeclBuilder
    # The signature declaration the builder is composing (class `Declaration`)
    attr_reader :decl

    class BuilderError < StandardError; end

    private def check_live!
      if decl.finalized
        raise BuilderError.new("You can't modify a signature declaration after it has been used.")
      end
    end

    private def check_sig_block_is_unset!
      if T::Private::DeclState.current.active_declaration&.blk
        raise BuilderError.new(
          "Cannot add more signature statements after the declaration block."
        )
      end
    end

    # Verify if we're trying to invoke the method outside of a signature block. Notice that we need to check if it's a
    # proc, because this is valid and would lead to a false positive: `T.type_alias { T.proc.params(a: Integer).void }`
    private def check_running_inside_block!(method_name)
      unless @inside_sig_block || decl.mod == T::Private::Methods::PROC_TYPE
        raise BuilderError.new(
          "Can't invoke #{method_name} outside of a signature declaration block"
        )
      end
    end

    def initialize(mod, raw)
      # TODO RUBYPLAT-1278 - with ruby 2.5, use kwargs here
      @decl = Declaration.new(
        mod,
        ARG_NOT_PROVIDED, # params
        ARG_NOT_PROVIDED, # returns
        ARG_NOT_PROVIDED, # bind
        Modes.standard, # mode
        ARG_NOT_PROVIDED, # checked
        false, # finalized
        ARG_NOT_PROVIDED, # on_failure
        nil, # override_allow_incompatible
        ARG_NOT_PROVIDED, # type_parameters
        raw,
        ARG_NOT_PROVIDED, # final
      )
      @inside_sig_block = false
    end

    def run!(&block)
      @inside_sig_block = true
      instance_exec(&block)
      finalize!
      self
    end

    def params(**params)
      check_live!
      check_running_inside_block!(__method__)

      if !decl.params.equal?(ARG_NOT_PROVIDED)
        raise BuilderError.new("You can't call .params twice")
      end

      if params.empty?
        raise BuilderError.new("params expects keyword arguments")
      end
      decl.params = params

      self
    end

    def returns(type)
      check_live!
      check_running_inside_block!(__method__)

      if decl.returns.is_a?(T::Private::Types::Void)
        raise BuilderError.new("You can't call .returns after calling .void.")
      end
      if !decl.returns.equal?(ARG_NOT_PROVIDED)
        raise BuilderError.new("You can't call .returns multiple times in a signature.")
      end

      decl.returns = type

      self
    end

    def void
      check_live!
      check_running_inside_block!(__method__)

      if !decl.returns.equal?(ARG_NOT_PROVIDED)
        raise BuilderError.new("You can't call .void after calling .returns.")
      end

      decl.returns = T::Private::Types::Void.new

      self
    end

    def bind(type)
      check_live!
      check_running_inside_block!(__method__)

      if !decl.bind.equal?(ARG_NOT_PROVIDED)
        raise BuilderError.new("You can't call .bind multiple times in a signature.")
      end

      decl.bind = type

      self
    end

    def checked(level)
      check_live!
      check_running_inside_block!(__method__)

      if !decl.checked.equal?(ARG_NOT_PROVIDED)
        raise BuilderError.new("You can't call .checked multiple times in a signature.")
      end
      if (level == :never || level == :compiled) && !decl.on_failure.equal?(ARG_NOT_PROVIDED)
        raise BuilderError.new("You can't use .checked(:#{level}) with .on_failure because .on_failure will have no effect.")
      end
      if !T::Private::RuntimeLevels::LEVELS.include?(level)
        raise BuilderError.new("Invalid `checked` level '#{level}'. Use one of: #{T::Private::RuntimeLevels::LEVELS}.")
      end

      decl.checked = level

      self
    end

    def on_failure(*args)
      check_live!
      check_running_inside_block!(__method__)

      if !decl.on_failure.equal?(ARG_NOT_PROVIDED)
        raise BuilderError.new("You can't call .on_failure multiple times in a signature.")
      end
      if decl.checked == :never || decl.checked == :compiled
        raise BuilderError.new("You can't use .on_failure with .checked(:#{decl.checked}) because .on_failure will have no effect.")
      end

      decl.on_failure = args

      self
    end

    def abstract(&blk)
      check_live!

      case decl.mode
      when Modes.standard
        decl.mode = Modes.abstract
      when Modes.abstract
        raise BuilderError.new(".abstract cannot be repeated in a single signature")
      else
        raise BuilderError.new("`.abstract` cannot be combined with `.override` or `.overridable`.")
      end

      check_sig_block_is_unset!

      if blk
        T::Private::DeclState.current.active_declaration.blk = blk
      end

      self
    end

    def final(&blk)
      check_live!

      if !decl.final.equal?(ARG_NOT_PROVIDED)
        raise BuilderError.new("You can't call .final multiple times in a signature.")
      end

      if @inside_sig_block
        raise BuilderError.new(
          "Unlike other sig annotations, the `final` annotation must remain outside the sig block, " \
          "using either `sig(:final) {...}` or `sig.final {...}`, not `sig {final. ...}"
        )
      end

      raise BuilderError.new(".final cannot be repeated in a single signature") if final?

      decl.final = true

      check_sig_block_is_unset!

      if blk
        T::Private::DeclState.current.active_declaration.blk = blk
      end

      self
    end

    def final?
      !decl.final.equal?(ARG_NOT_PROVIDED) && decl.final
    end

    def override(allow_incompatible: false, &blk)
      check_live!

      case decl.mode
      when Modes.standard
        decl.mode = Modes.override
        decl.override_allow_incompatible = allow_incompatible
      when Modes.override, Modes.overridable_override
        raise BuilderError.new(".override cannot be repeated in a single signature")
      when Modes.overridable
        decl.mode = Modes.overridable_override
      else
        raise BuilderError.new("`.override` cannot be combined with `.abstract`.")
      end

      check_sig_block_is_unset!

      if blk
        T::Private::DeclState.current.active_declaration.blk = blk
      end

      self
    end

    def overridable(&blk)
      check_live!

      case decl.mode
      when Modes.abstract
        raise BuilderError.new("`.overridable` cannot be combined with `.#{decl.mode}`")
      when Modes.override
        decl.mode = Modes.overridable_override
      when Modes.standard
        decl.mode = Modes.overridable
      when Modes.overridable, Modes.overridable_override
        raise BuilderError.new(".overridable cannot be repeated in a single signature")
      end

      check_sig_block_is_unset!

      if blk
        T::Private::DeclState.current.active_declaration.blk = blk
      end

      self
    end

    # Declares valid type paramaters which can be used with `T.type_parameter` in
    # this `sig`.
    #
    # This is used for generic methods. Example usage:
    #
    #  sig do
    #    type_parameters(:U)
    #    .params(blk: T.proc.params(arg0: Elem).returns(T.type_parameter(:U)))
    #    .returns(T::Array[T.type_parameter(:U)])
    #  end
    #  def map(&blk); end
    def type_parameters(*names)
      check_live!
      check_running_inside_block!(__method__)

      names.each do |name|
        raise BuilderError.new("not a symbol: #{name}") unless name.is_a?(Symbol)
      end

      if !decl.type_parameters.equal?(ARG_NOT_PROVIDED)
        raise BuilderError.new("You can't call .type_parameters multiple times in a signature.")
      end

      decl.type_parameters = names

      self
    end

    def finalize!
      check_live!

      if decl.returns.equal?(ARG_NOT_PROVIDED)
        raise BuilderError.new("You must provide a return type; use the `.returns` or `.void` builder methods.")
      end

      if decl.bind.equal?(ARG_NOT_PROVIDED)
        decl.bind = nil
      end
      if decl.checked.equal?(ARG_NOT_PROVIDED)
        default_checked_level = T::Private::RuntimeLevels.default_checked_level
        if (default_checked_level == :never || default_checked_level == :compiled) && !decl.on_failure.equal?(ARG_NOT_PROVIDED)
          raise BuilderError.new("To use .on_failure you must additionally call .checked(:tests) or .checked(:always), otherwise, the .on_failure has no effect.")
        end
        decl.checked = default_checked_level
      end
      if decl.on_failure.equal?(ARG_NOT_PROVIDED)
        decl.on_failure = nil
      end
      if decl.params.equal?(ARG_NOT_PROVIDED)
        decl.params = {}
      end
      if decl.type_parameters.equal?(ARG_NOT_PROVIDED)
        decl.type_parameters = {}
      end

      decl.finalized = true

      self
    end
  end
end