# frozen_string_literal: true module LIT # @api private # @since 0.1.0 class TypeChecker AST = Parser::AST PRIMITIVE_TYPE_NAMESPACE = "AST::Type::Primitive" def initialize(mod) @mod = mod end def check_type!(type, value) is_primitive = if type.is_a?(Module) type.name.include?(PRIMITIVE_TYPE_NAMESPACE) else type.class.name.include?(PRIMITIVE_TYPE_NAMESPACE) end if is_primitive check_primitive_type!(type, value) elsif type.is_a?(AST::Type::Alias) check_type_alias!(type.name, value) else raise InvalidTypeError, "invalid type: #{type}" end end private # rubocop:disable Metrics/MethodLength def check_primitive_type!(type, value) if type == AST::Type::Primitive::String unless value.is_a?(String) raise TypeError, "expected #{value} to be a String" end elsif type == AST::Type::Primitive::Integer unless value.is_a?(Integer) raise TypeError, "expected #{value} to be an Integer" end elsif type == AST::Type::Primitive::Float unless value.is_a?(Float) raise TypeError, "expected #{value} to be a Float" end elsif type == AST::Type::Primitive::Boolean unless value == true || value == false raise TypeError, "expected #{value} to be a Boolean" end elsif type == AST::Type::Primitive::Unit unless value.nil? raise TypeError, "expected #{value} to be a Unit (nil)" end elsif type.is_a?(AST::Type::Primitive::Option) if value.is_a?(::LIT::Object::Option::Some) check_primitive_type!(type.type, value.value) elsif value == ::LIT::Object::Option::None # ok else raise TypeError, "expected #{value} to be an instance of Option" end elsif type.is_a?(AST::Type::Primitive::Array) if value.is_a?(::LIT::Object::Array) value.each { |v| check_primitive_type!(type.type, v) } else raise TypeError, "expected #{value} to be an array" end elsif type.is_a?(AST::Type::Primitive::Map) if value.is_a?(::LIT::Object::Map) value.values.each do |(k, v)| check_primitive_type!(type.key_type, k) check_primitive_type!(type.value_type, v) end else raise TypeError, "expected #{value} to be a Map" end elsif type.is_a?(AST::Type::Alias) check_type_alias!(type.name, value) else raise InvalidTypeError, "invalid primitive type: #{type}" end end # rubocop:enable Metrics/MethodLength def check_type_alias!(name, value) const = @mod.const_get(name) if const <= Object::Enum unless value.respond_to?(:__parent__) && value.__parent__ == const raise TypeError, "expected #{value} to be of type #{name}" end elsif const <= Object::Struct # ok (checked upon struct initialization) elsif const <= Object::Option type = const.const_get("TYPE") check_type!(type, value) elsif const <= Object::Map type = const.const_get("TYPE") check_type!(type, value) elsif const <= Object::Array type = const.const_get("TYPE") check_type!(type, value) else type = const.const_get("TYPE") check_primitive_type!(type, value) end end end end