File: //usr/local/rvm/rubies/default/lib/ruby/gems/3.0.0/gems/rbs-1.0.4/lib/rbs/test/type_check.rb
module RBS
module Test
class TypeCheck
attr_reader :self_class
attr_reader :builder
attr_reader :sample_size
attr_reader :unchecked_classes
attr_reader :const_cache
DEFAULT_SAMPLE_SIZE = 100
def initialize(self_class:, builder:, sample_size:, unchecked_classes:)
@self_class = self_class
@builder = builder
@sample_size = sample_size
@unchecked_classes = unchecked_classes.uniq
@const_cache = {}
end
def overloaded_call(method, method_name, call, errors:)
es = method.method_types.map do |method_type|
es = method_call(method_name, method_type, call, errors: [])
if es.empty?
return errors
else
es
end
end
if es.size == 1
errors.push(*es[0])
else
errors << Errors::UnresolvedOverloadingError.new(
klass: self_class,
method_name: method_name,
method_types: method.method_types
)
end
errors
end
def method_call(method_name, method_type, call, errors:)
args(method_name, method_type, method_type.type, call.method_call, errors, type_error: Errors::ArgumentTypeError, argument_error: Errors::ArgumentError)
self.return(method_name, method_type, method_type.type, call.method_call, errors, return_error: Errors::ReturnTypeError)
if method_type.block
case
when !call.block_calls.empty?
call.block_calls.each do |block_call|
args(method_name, method_type, method_type.block.type, block_call, errors, type_error: Errors::BlockArgumentTypeError, argument_error: Errors::BlockArgumentError)
self.return(method_name, method_type, method_type.block.type, block_call, errors, return_error: Errors::BlockReturnTypeError)
end
when !call.block_given
# Block is not given
if method_type.block.required
errors << Errors::MissingBlockError.new(klass: self_class, method_name: method_name, method_type: method_type)
end
else
# Block is given, but not yielded
end
else
if call.block_given
errors << Errors::UnexpectedBlockError.new(klass: self_class, method_name: method_name, method_type: method_type)
end
end
errors
end
def args(method_name, method_type, fun, call, errors, type_error:, argument_error:)
test = zip_args(call.arguments, fun) do |val, param|
unless self.value(val, param.type)
errors << type_error.new(klass: self_class,
method_name: method_name,
method_type: method_type,
param: param,
value: val)
end
end
unless test
errors << argument_error.new(klass: self_class,
method_name: method_name,
method_type: method_type)
end
end
def return(method_name, method_type, fun, call, errors, return_error:)
if call.return?
unless value(call.return_value, fun.return_type)
errors << return_error.new(klass: self_class,
method_name: method_name,
method_type: method_type,
type: fun.return_type,
value: call.return_value)
end
end
end
def zip_keyword_args(hash, fun)
fun.required_keywords.each do |name, param|
if hash.key?(name)
yield(hash[name], param)
else
return false
end
end
fun.optional_keywords.each do |name, param|
if hash.key?(name)
yield(hash[name], param)
end
end
hash.each do |name, value|
next if fun.required_keywords.key?(name)
next if fun.optional_keywords.key?(name)
if fun.rest_keywords
yield value, fun.rest_keywords
else
return false
end
end
true
end
def keyword?(value)
value.is_a?(Hash) && value.keys.all? {|key| key.is_a?(Symbol) }
end
def zip_args(args, fun, &block)
case
when args.empty?
if fun.required_positionals.empty? && fun.trailing_positionals.empty? && fun.required_keywords.empty?
true
else
false
end
when !fun.required_positionals.empty?
yield_self do
param, fun_ = fun.drop_head
yield(args.first, param)
zip_args(args.drop(1), fun_, &block)
end
when fun.has_keyword?
yield_self do
hash = args.last
if keyword?(hash)
zip_keyword_args(hash, fun, &block) &&
zip_args(args.take(args.size - 1),
fun.update(required_keywords: {}, optional_keywords: {}, rest_keywords: nil),
&block)
else
fun.required_keywords.empty? &&
zip_args(args,
fun.update(required_keywords: {}, optional_keywords: {}, rest_keywords: nil),
&block)
end
end
when !fun.trailing_positionals.empty?
yield_self do
param, fun_ = fun.drop_tail
yield(args.last, param)
zip_args(args.take(args.size - 1), fun_, &block)
end
when !fun.optional_positionals.empty?
yield_self do
param, fun_ = fun.drop_head
yield(args.first, param)
zip_args(args.drop(1), fun_, &block)
end
when fun.rest_positionals
yield_self do
yield(args.first, fun.rest_positionals)
zip_args(args.drop(1), fun, &block)
end
else
false
end
end
def each_sample(array, &block)
if block
if sample_size && array.size > sample_size
if sample_size > 0
size = array.size
sample_size.times do
yield array[rand(size)]
end
end
else
array.each(&block)
end
else
enum_for :each_sample, array
end
end
def get_class(type_name)
const_cache[type_name] ||= begin
Object.const_get(type_name.to_s)
rescue NameError
nil
end
end
def is_double?(value)
unchecked_classes.any? { |unchecked_class| Test.call(value, IS_AP, Object.const_get(unchecked_class))}
rescue NameError
false
end
def value(val, type)
if is_double?(val)
RBS.logger.info("A double (#{val.inspect}) is detected!")
return true
end
case type
when Types::Bases::Any
true
when Types::Bases::Bool
val.is_a?(TrueClass) || val.is_a?(FalseClass)
when Types::Bases::Top
true
when Types::Bases::Bottom
false
when Types::Bases::Void
true
when Types::Bases::Self
Test.call(val, IS_AP, self_class)
when Types::Bases::Nil
Test.call(val, IS_AP, ::NilClass)
when Types::Bases::Class
Test.call(val, IS_AP, Class)
when Types::Bases::Instance
Test.call(val, IS_AP, self_class)
when Types::ClassInstance
klass = get_class(type.name) or return false
case
when klass == ::Array
Test.call(val, IS_AP, klass) && each_sample(val).all? {|v| value(v, type.args[0]) }
when klass == ::Hash
Test.call(val, IS_AP, klass) && each_sample(val.keys).all? do |key|
value(key, type.args[0]) && value(val[key], type.args[1])
end
when klass == ::Range
Test.call(val, IS_AP, klass) && value(val.begin, type.args[0]) && value(val.end, type.args[0])
when klass == ::Enumerator
if Test.call(val, IS_AP, klass)
case val.size
when Float::INFINITY
values = []
ret = self
val.lazy.take(10).each do |*args|
values << args
nil
end
else
values = []
ret = val.each do |*args|
values << args
nil
end
end
each_sample(values).all? do |v|
if v.size == 1
# Only one block argument.
value(v[0], type.args[0]) || value(v, type.args[0])
else
value(v, type.args[0])
end
end &&
if ret.equal?(self)
type.args[1].is_a?(Types::Bases::Bottom)
else
value(ret, type.args[1])
end
end
else
Test.call(val, IS_AP, klass)
end
when Types::ClassSingleton
klass = get_class(type.name) or return false
singleton_class = begin
klass.singleton_class
rescue TypeError
return false
end
val.is_a?(singleton_class)
when Types::Interface
methods = Set.new(Test.call(val, METHODS))
if (definition = builder.build_interface(type.name))
definition.methods.each_key.all? do |method_name|
methods.member?(method_name)
end
end
when Types::Variable
true
when Types::Literal
val == type.literal
when Types::Union
type.types.any? {|type| value(val, type) }
when Types::Intersection
type.types.all? {|type| value(val, type) }
when Types::Optional
Test.call(val, IS_AP, ::NilClass) || value(val, type.type)
when Types::Alias
value(val, builder.expand_alias(type.name))
when Types::Tuple
Test.call(val, IS_AP, ::Array) &&
type.types.map.with_index {|ty, index| value(val[index], ty) }.all?
when Types::Record
Test::call(val, IS_AP, ::Hash) &&
type.fields.map {|key, type| value(val[key], type) }.all?
when Types::Proc
Test::call(val, IS_AP, ::Proc)
else
false
end
end
end
end
end