lib/steep/signature/validator.rb



module Steep
  module Signature
    class Validator
      Location = RBS::Location
      Declarations = RBS::AST::Declarations

      attr_reader :checker

      def initialize(checker:)
        @checker = checker
        @errors = []
      end

      def has_error?
        !no_error?
      end

      def no_error?
        @errors.empty?
      end

      def each_error(&block)
        if block_given?
          @errors.each(&block)
        else
          enum_for :each_error
        end
      end

      def env
        checker.factory.env
      end

      def builder
        checker.factory.definition_builder
      end

      def type_name_resolver
        @type_name_resolver ||= RBS::TypeNameResolver.from_env(env)
      end

      def validator
        @validator ||= RBS::Validator.new(env: env, resolver: type_name_resolver)
      end

      def factory
        checker.factory
      end

      def validate
        @errors = []

        validate_decl
        validate_const
        validate_global
        validate_alias
      end

      def validate_type(type)
        Steep.logger.debug "#{Location.to_string type.location}: Validating #{type}..."
        validator.validate_type type, context: [RBS::Namespace.root]
      end

      def ancestor_to_type(ancestor)
        case ancestor
        when RBS::Definition::Ancestor::Instance
          args = ancestor.args.map {|type| checker.factory.type(type) }

          case
          when ancestor.name.interface?
            AST::Types::Name::Interface.new(name: ancestor.name, args: args, location: nil)
          when ancestor.name.class?
            AST::Types::Name::Instance.new(name: ancestor.name, args: args, location: nil)
          else
            raise "#{ancestor.name}"
          end
        else
          raise "Unexpected ancestor: #{ancestor.inspect}"
        end
      end

      def mixin_constraints(definition, mixin_ancestors, immediate_self_types:)
        relations = []

        self_type = checker.factory.type(definition.self_type)
        if immediate_self_types && !immediate_self_types.empty?
          self_type = AST::Types::Intersection.build(
            types: immediate_self_types.map {|st| ancestor_to_type(st) }.push(self_type),
            location: nil
          )
        end

        mixin_ancestors.each do |ancestor|
          args = ancestor.args.map {|type| checker.factory.type(type) }
          ancestor_ancestors = builder.ancestor_builder.one_instance_ancestors(ancestor.name)
          self_constraints = ancestor_ancestors.self_types.map do |self_ancestor|
            s = Interface::Substitution.build(ancestor_ancestors.params, args)
            ancestor_to_type(self_ancestor).subst(s)
          end

          self_constraints.each do |constraint|
            relations << [
              Subtyping::Relation.new(sub_type: self_type, super_type: constraint),
              ancestor
            ]
          end
        end

        relations
      end

      def validate_one_class(name)
        rescue_validation_errors(name) do
          Steep.logger.debug { "Validating class definition `#{name}`..." }

          Steep.logger.tagged "#{name}" do
            builder.build_instance(name).tap do |definition|
              definition.instance_variables.each do |name, var|
                if parent = var.parent_variable
                  var_type = checker.factory.type(var.type)
                  parent_type = checker.factory.type(parent.type)

                  relation = Subtyping::Relation.new(sub_type: var_type, super_type: parent_type)
                  result1 = checker.check(
                    relation,
                    self_type: AST::Types::Self.new,
                    instance_type: AST::Types::Instance.new,
                    class_type: AST::Types::Class.new,
                    constraints: Subtyping::Constraints.empty
                  )
                  result2 = checker.check(
                    relation.flip,
                    self_type: AST::Types::Self.new,
                    instance_type: AST::Types::Instance.new,
                    class_type: AST::Types::Class.new,
                    constraints: Subtyping::Constraints.empty
                  )

                  unless result1.success? and result2.success?
                    @errors << Diagnostic::Signature::InstanceVariableTypeError.new(
                      name: name,
                      location: var.type.location,
                      var_type: var_type,
                      parent_type: parent_type
                    )
                  end
                end
              end

              ancestors = builder.ancestor_builder.one_instance_ancestors(name)
              mixin_constraints(definition, ancestors.included_modules, immediate_self_types: ancestors.self_types).each do |relation, ancestor|
                checker.check(
                  relation,
                  self_type: AST::Types::Self.new,
                  instance_type: AST::Types::Instance.new,
                  class_type: AST::Types::Class.new,
                  constraints: Subtyping::Constraints.empty
                ).else do
                  @errors << Diagnostic::Signature::ModuleSelfTypeError.new(
                    name: name,
                    location: ancestor.source&.location || raise,
                    ancestor: ancestor,
                    relation: relation
                  )
                end
              end

              definition.each_type do |type|
                validate_type type
              end
            end

            builder.build_singleton(name).tap do |definition|
              definition.instance_variables.each do |name, var|
                if parent = var.parent_variable
                  var_type = checker.factory.type(var.type)
                  parent_type = checker.factory.type(parent.type)

                  relation = Subtyping::Relation.new(sub_type: var_type, super_type: parent_type)
                  result1 = checker.check(
                    relation,
                    self_type: AST::Types::Self.new,
                    instance_type: AST::Types::Instance.new,
                    class_type: AST::Types::Class.new,
                    constraints: Subtyping::Constraints.empty
                  )
                  result2 = checker.check(
                    relation.flip,
                    self_type: AST::Types::Self.new,
                    instance_type: AST::Types::Instance.new,
                    class_type: AST::Types::Class.new,
                    constraints: Subtyping::Constraints.empty
                  )

                  unless result1.success? and result2.success?
                    @errors << Diagnostic::Signature::InstanceVariableTypeError.new(
                      name: name,
                      location: var.type.location,
                      var_type: var_type,
                      parent_type: parent_type
                    )
                  end
                end
              end

              ancestors = builder.ancestor_builder.one_singleton_ancestors(name)
              mixin_constraints(definition, ancestors.extended_modules, immediate_self_types: ancestors.self_types).each do |relation, ancestor|
                checker.check(
                  relation,
                  self_type: AST::Types::Self.new,
                  instance_type: AST::Types::Instance.new,
                  class_type: AST::Types::Class.new,
                  constraints: Subtyping::Constraints.empty
                ).else do
                  @errors << Diagnostic::Signature::ModuleSelfTypeError.new(
                    name: name,
                    location: ancestor.source&.location || raise,
                    ancestor: ancestor,
                    relation: relation
                  )
                end
              end

              definition.each_type do |type|
                validate_type type
              end
            end
          end
        end
      end

      def validate_one_interface(name)
        rescue_validation_errors(name) do
          Steep.logger.debug "Validating interface `#{name}`..."
          Steep.logger.tagged "#{name}" do
            builder.build_interface(name).each_type do |type|
              validate_type type
            end
          end
        end
      end

      def validate_decl
        env.class_decls.each_key do |name|
          validate_one_class(name)
        end

        env.interface_decls.each_key do |name|
          validate_one_interface(name)
        end
      end

      def validate_const
        env.constant_decls.each do |name, entry|
          validate_one_constant(name, entry)
        end
      end

      def validate_one_constant(name, entry)
        rescue_validation_errors do
          Steep.logger.debug "Validating constant `#{name}`..."
          builder.ensure_namespace!(name.namespace, location: entry.decl.location)
          validate_type entry.decl.type
        end
      end

      def validate_global
        env.global_decls.each do |name, entry|
          validate_one_global(name, entry)
        end
      end

      def validate_one_global(name, entry)
        rescue_validation_errors do
          Steep.logger.debug "Validating global `#{name}`..."
          validate_type entry.decl.type
        end
      end

      def validate_one_alias(name)
        rescue_validation_errors(name) do
          Steep.logger.debug "Validating alias `#{name}`..."
          builder.expand_alias(name).tap do |type|
            validate_type(type)
          end
        end
      end

      def validate_alias
        env.alias_decls.each do |name, entry|
          validate_one_alias(name)
        end
      end

      def rescue_validation_errors(type_name = nil)
        yield
      rescue RBS::ErrorBase => exn
        @errors << Diagnostic::Signature.from_rbs_error(exn, factory: factory)
      end
    end
  end
end