lib/steep/interface/instantiated.rb



module Steep
  module Interface
    class Instantiated
      attr_reader :type
      attr_reader :methods
      attr_reader :ivar_chains

      def initialize(type:, methods:, ivar_chains:)
        @type = type
        @methods = methods
        @ivar_chains = ivar_chains
      end

      def ivars
        @ivars ||= ivar_chains.transform_values(&:type)
      end

      def ==(other)
        other.is_a?(self.class) && other.type == type && other.params == params && other.methods == methods && other.ivars == ivars
      end

      def subst(s)
        self.class.new(
          type: type,
          methods: methods.transform_values {|type| type.subst(s) },
          ivar_chains: ivar_chains.transform_values {|chain| chain.subst(s) }
        )
      end

      class InvalidMethodOverrideError < StandardError
        attr_reader :type
        attr_reader :current_method
        attr_reader :super_method
        attr_reader :result

        def initialize(type:, current_method:, super_method:, result:)
          @type = type
          @current_method = current_method
          @super_method = super_method
          @result = result

          super "Invalid override of `#{current_method.name}` in #{type}: definition in #{current_method.type_name} is not compatible with its super (#{super_method.type_name})"
        end
      end

      class InvalidIvarOverrideError < StandardError
        attr_reader :type
        attr_reader :ivar_name
        attr_reader :current_ivar_type
        attr_reader :super_ivar_type

        def initialize(type:, ivar_name:, current_ivar_type:, super_ivar_type:)
          @type = type
          @ivar_name = ivar_name
          @current_ivar_type = current_ivar_type
          @super_ivar_type = super_ivar_type

          super "Invalid override of `#{ivar_name}` in #{type}: #{current_ivar_type} is not compatible with #{super_ivar_type}"
        end
      end

      def validate(check)
        methods.each do |_, method|
          validate_method(check, method)
        end

        ivar_chains.each do |name, chain|
          validate_chain(check, name, chain)
        end
      end

      def validate_chain(check, name, chain)
        return unless chain.parent

        this_type = chain.type
        super_type = chain.parent.type

        case
        when this_type.is_a?(AST::Types::Any) && super_type.is_a?(AST::Types::Any)
          # ok
        else
          relation = Subtyping::Relation.new(sub_type: this_type, super_type: super_type)

          result1 = check.check(relation, constraints: Subtyping::Constraints.empty)
          result2 = check.check(relation.flip, constraints: Subtyping::Constraints.empty)

          if result1.failure? || result2.failure? || this_type.is_a?(AST::Types::Any) || super_type.is_a?(AST::Types::Any)
            raise InvalidIvarOverrideError.new(type: self.type, ivar_name: name, current_ivar_type: this_type, super_ivar_type: super_type)
          end
        end

        validate_chain(check, name, chain.parent)
      end

      def validate_method(check, method)
        if method.super_method && !method.incompatible?
          result = check.check_method(method.name,
                                      method,
                                      method.super_method,
                                      assumption: Set.new,
                                      trace: Subtyping::Trace.new,
                                      constraints: Subtyping::Constraints.empty)

          if result.success?
            validate_method(check, method.super_method)
          else
            raise InvalidMethodOverrideError.new(type: type,
                                                 current_method: method,
                                                 super_method: method.super_method,
                                                 result: result)
          end
        end
      end

      def select_method_type(&block)
        self.class.new(
          type: type,
          methods: methods.each.with_object({}) do |(name, method), methods|
            methods[name] = Method.new(
              type_name: method.type_name,
              name: method.name,
              types: method.types.select(&block),
              super_method: method.super_method,
              attributes: method.attributes,
            )
          end.reject do |_, method|
            method.types.empty?
          end,
          ivar_chains: ivar_chains
        )
      end
    end
  end
end