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
          @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::Resolver::TypeNameResolver.new(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_application_constraints(type_name, type_params, type_args, location:)
        if type_params.size == type_args.size
          subst = Interface::Substitution.build(
            type_params.map(&:name),
            type_args.map {|type| factory.type(type) }
          )

          type_params.zip(type_args).each do |param, arg|
            arg or raise

            if param.upper_bound
              upper_bound_type = factory.type(param.upper_bound).subst(subst)
              arg_type = factory.type(arg)

              constraints = Subtyping::Constraints.empty

              checker.check(
                Subtyping::Relation.new(sub_type: arg_type, super_type: upper_bound_type),
                self_type: AST::Types::Self.instance,
                class_type: nil,
                instance_type: nil,
                constraints: constraints
              ).else do |result|
                @errors << Diagnostic::Signature::UnsatisfiableTypeApplication.new(
                  type_name: type_name,
                  type_arg: arg_type,
                  type_param: Interface::TypeParam.new(
                    name: param.name,
                    upper_bound: upper_bound_type,
                    variance: param.variance,
                    unchecked: param.unchecked?
                  ),
                  location: location
                )
              end
            end
          end
        end
      end

      def validate_type_application(type)
        name, type_params, type_args =
          case type
          when RBS::Types::ClassInstance
            [
              type.name,
              builder.build_instance(type.name).type_params_decl,
              type.args
            ]
          when RBS::Types::Interface
            [
              type.name,
              builder.build_interface(type.name).type_params_decl,
              type.args
            ]
          when RBS::Types::Alias
            type_name = env.normalize_type_name?(type.name) or return
            entry = env.type_alias_decls[type_name]

            [
              type_name,
              entry.decl.type_params,
              type.args
            ]
          end

        if name && type_params && type_args
          if !type_params.empty? && !type_args.empty?
            validate_type_application_constraints(name, type_params, type_args, location: type.location)
          end
        end

        type.each_type do |child|
          validate_type_application(child)
        end
      end

      def validate_type(type)
        Steep.logger.debug { "#{Location.to_string type.location}: Validating #{type}..." }

        validator.validate_type(type, context: nil)
        validate_type_application(type)
      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:)
        # @type var relations: Array[[Subtyping::Relation[AST::Types::t], RBS::Definition::Ancestor::Instance]]
        relations = []

        self_type = checker.factory.type(definition.self_type)
        if immediate_self_types && !immediate_self_types.empty?
          # @type var sts: Array[AST::Types::t]
          sts = immediate_self_types.map {|st| ancestor_to_type(st) }
          self_type = AST::Types::Intersection.build(types: sts.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)
          ancestor_ancestors.self_types or raise
          ancestor_ancestors.params or raise
          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 each_method_type(definition)
        type_name = definition.type_name

        definition.methods.each_value do |method|
          if method.defined_in == type_name
            method.method_types.each do |method_type|
              yield method_type
            end
          end
        end
      end

      def each_variable_type(definition)
        type_name = definition.type_name

        definition.instance_variables.each_value do |var|
          if var.declared_in == type_name
            yield var.type
          end
        end

        definition.class_variables.each_value do |var|
          if var.declared_in == type_name
            yield var.type
          end
        end
      end

      def validate_definition_type(definition)
        each_method_type(definition) do |method_type|
          upper_bounds = method_type.type_params.each.with_object({}) do |param, hash|
            hash[param.name] = factory.type_opt(param.upper_bound)
          end

          checker.push_variable_bounds(upper_bounds) do
            method_type.each_type do |type|
              validate_type(type)
            end
          end
        end

        each_variable_type(definition) do |type|
          validate_type(type)
        end
      end

      def validate_one_class_decl(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|
              upper_bounds = definition.type_params_decl.each.with_object({}) do |param, bounds|
                bounds[param.name] = factory.type_opt(param.upper_bound)
              end

              checker.push_variable_bounds(upper_bounds) do
                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: nil, instance_type: nil, class_type: nil, constraints: Subtyping::Constraints.empty)
                    result2 = checker.check(relation.flip, self_type: nil, instance_type: nil, class_type: nil, 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 || raise, immediate_self_types: ancestors.self_types).each do |relation, ancestor|
                  checker.check(
                    relation,
                    self_type: AST::Types::Self.instance,
                    instance_type: AST::Types::Instance.instance,
                    class_type: AST::Types::Class.instance,
                    constraints: Subtyping::Constraints.empty
                  ).else do
                    raise if ancestor.source.is_a?(Symbol)

                    @errors << Diagnostic::Signature::ModuleSelfTypeError.new(
                      name: name,
                      location: ancestor.source&.location || raise,
                      ancestor: ancestor,
                      relation: relation
                    )
                  end
                end

                ancestors.each_ancestor do |ancestor|
                  case ancestor
                  when RBS::Definition::Ancestor::Instance
                    validate_ancestor_application(name, ancestor)
                  end
                end

                validate_definition_type(definition)
              end
            end

            builder.build_singleton(name).tap do |definition|
              entry =
                case definition.entry
                when RBS::Environment::ClassEntry, RBS::Environment::ModuleEntry
                  definition.entry
                else
                  raise
                end

              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.instance,
                    instance_type: AST::Types::Instance.instance,
                    class_type: AST::Types::Class.instance,
                    constraints: Subtyping::Constraints.empty
                  )
                  result2 = checker.check(
                    relation.flip,
                    self_type: AST::Types::Self.instance,
                    instance_type: AST::Types::Instance.instance,
                    class_type: AST::Types::Class.instance,
                    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

              definition.class_variables.each do |name, var|
                if var.declared_in == definition.type_name
                  if (parent = var.parent_variable) && var.declared_in != parent.declared_in
                    class_var = entry.decls.flat_map {|decl| decl.decl.members }.find do |member|
                      member.is_a?(RBS::AST::Members::ClassVariable) && member.name == name
                    end

                    if class_var
                      loc = class_var.location #: RBS::Location[untyped, untyped]?
                      @errors << Diagnostic::Signature::ClassVariableDuplicationError.new(
                        class_name: definition.type_name,
                        other_class_name: parent.declared_in,
                        variable_name: name,
                        location: loc&.[](:name) || raise
                      )
                    end
                  end
                end
              end

              ancestors = builder.ancestor_builder.one_singleton_ancestors(name)
              ancestors.extended_modules or raise
              mixin_constraints(definition, ancestors.extended_modules, immediate_self_types: ancestors.self_types).each do |relation, ancestor|
                checker.check(
                  relation,
                  self_type: AST::Types::Self.instance ,
                  instance_type: AST::Types::Instance.instance,
                  class_type: AST::Types::Class.instance,
                  constraints: Subtyping::Constraints.empty
                ).else do
                  raise if ancestor.source.is_a?(Symbol)

                  @errors << Diagnostic::Signature::ModuleSelfTypeError.new(
                    name: name,
                    location: ancestor.source&.location || raise,
                    ancestor: ancestor,
                    relation: relation
                  )
                end
              end
              ancestors.each_ancestor do |ancestor|
                case ancestor
                when RBS::Definition::Ancestor::Instance
                  validate_ancestor_application(name, ancestor)
                end
              end

              validate_definition_type(definition)
            end
          end
        end
      end

      def validate_one_class(name)
        entry = env.constant_entry(name)

        case entry
        when RBS::Environment::ClassEntry, RBS::Environment::ModuleEntry
          validate_one_class_decl(name)
        when RBS::Environment::ClassAliasEntry, RBS::Environment::ModuleAliasEntry
          validate_one_class_alias(name, entry)
        end
      end

      def validate_ancestor_application(name, ancestor)
        unless ancestor.args.empty?
          definition =
            case
            when ancestor.name.class?
              builder.build_instance(ancestor.name)
            when ancestor.name.interface?
              builder.build_interface(ancestor.name)
            else
              raise
            end

          location =
            case ancestor.source
            when :super
              primary_decl = env.class_decls[name].primary.decl
              primary_decl.is_a?(RBS::AST::Declarations::Class) or raise
              if super_class = primary_decl.super_class
                super_class.location
              else
                # Implicit super class (Object): this can be skipped in fact...
                primary_decl.location&.aref(:name)
              end
            else
              ancestor.source&.location
            end

          validate_type_application_constraints(
            ancestor.name,
            definition.type_params_decl,
            ancestor.args,
            location: location
          )

          ancestor.args.each do |arg|
            validate_type(arg)
          end
        end
      end

      def validate_one_interface(name)
        rescue_validation_errors(name) do
          Steep.logger.debug "Validating interface `#{name}`..."
          Steep.logger.tagged "#{name}" do
            definition = builder.build_interface(name)

            upper_bounds = definition.type_params_decl.each.with_object({}) do |param, bounds|
              bounds[param.name] = factory.type_opt(param.upper_bound)
            end

            checker.push_variable_bounds(upper_bounds) do
              validate_definition_type(definition)

              ancestors = builder.ancestor_builder.one_interface_ancestors(name)
              ancestors.each_ancestor do |ancestor|
                case ancestor
                when RBS::Definition::Ancestor::Instance
                  # Interface ancestor cannot be other than Interface
                  ancestor.source.is_a?(Symbol) and raise

                  defn = builder.build_interface(ancestor.name)
                  validate_type_application_constraints(
                    ancestor.name,
                    defn.type_params_decl,
                    ancestor.args,
                    location: ancestor.source&.location || raise
                  )
                end
              end
            end
          end
        end
      end

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

        env.class_alias_decls.each do |name, entry|
          validate_one_class_alias(name, entry)
        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, entry = env.type_alias_decls[name])
        rescue_validation_errors(name) do
          Steep.logger.debug "Validating alias `#{name}`..."

          unless name.namespace.empty?
            outer = name.namespace.to_type_name
            builder.validate_type_name(outer, entry.decl.location&.aref(:name))
          end

          upper_bounds = entry.decl.type_params.each.with_object({}) do |param, bounds|
            bounds[param.name] = factory.type_opt(param.upper_bound)
          end

          validator.validate_type_alias(entry: entry) do |type|
            checker.push_variable_bounds(upper_bounds) do
              validate_type(entry.decl.type)
            end
          end
        end
      end

      def validate_one_class_alias(name, entry)
        rescue_validation_errors(name) do
          Steep.logger.debug "Validating class/module alias `#{name}`..."
          validator.validate_class_alias(entry: entry)
        end
      end

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

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