Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion lib/rbs/test/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ def self.inspect_(obj)
end

def self.method_tag(error)
if error.klass.singleton_class?
case
when error.klass.nil?
name = ""
method_name = error.method_name.to_s
when error.klass.singleton_class?
name = inspect_(error.klass).sub(/\A#<Class:(.*)>\z/, '\1')
method_name = ".#{error.method_name}"
else
Expand Down
31 changes: 28 additions & 3 deletions lib/rbs/test/tester.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,33 @@ def env
builder.env
end

def check
@check ||= TypeCheck.new(self_class: self_class, builder: builder, sample_size: sample_size, unchecked_classes: unchecked_classes)
def receiver_context(receiver)
case kind
when :instance
if extended_call?(receiver)
TypeCheck::SingletonContext.of(receiver)
else
TypeCheck::InstanceContext.of(receiver)
end
when :singleton
TypeCheck::SingletonContext.of(receiver)
else
raise
end
end

def extended_call?(receiver)
Test.call(receiver, IS_AP, Module) && !(Test.call(receiver, CLASS) <= self_class)
end

def check(receiver)
TypeCheck.new(
context: receiver_context(receiver),
owner: self_class,
builder: builder,
sample_size: sample_size,
unchecked_classes: unchecked_classes
)
end

def format_method_name(name)
Expand All @@ -149,7 +174,7 @@ def call(receiver, trace)
method = definition.methods[method_name]
if method
RBS.logger.debug { "Type checking `#{self_class}#{format_method_name(method_name)}`..."}
errors = check.overloaded_call(method, method_name, trace, errors: [])
errors = check(receiver).overloaded_call(method, method_name, trace, errors: [])

if errors.empty?
RBS.logger.debug { "No type error detected 👏" }
Expand Down
113 changes: 97 additions & 16 deletions lib/rbs/test/type_check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,105 @@
module RBS
module Test
class TypeCheck
attr_reader :self_class
InstanceContext = Data.define(:klass) do
def self.of(receiver)
new(klass: Test.call(receiver, CLASS))
end

def self_class
instance_class
end

def instance_class
klass
end

def class_class
Test.call(klass, SINGLETON_CLASS)
end
end

SingletonContext = Data.define(:klass) do
def self.of(receiver)
new(klass: receiver)
end

def self_class
class_class
end

def instance_class
klass
end

def class_class
Test.call(klass, SINGLETON_CLASS)
end
end

class NoReceiverContextError < StandardError
attr_reader :type

def initialize(type)
@type = type
super "`#{type}` type cannot be checked without a receiver context"
end
end

LegacyContext = Struct.new(:self_class, :instance_class, :class_class, keyword_init: true)
private_constant :LegacyContext

attr_reader :context
attr_reader :owner
attr_reader :builder
attr_reader :sample_size
attr_reader :unchecked_classes
attr_reader :instance_class
attr_reader :class_class

DEFAULT_SAMPLE_SIZE = 100

def initialize(self_class:, builder:, sample_size:, unchecked_classes:, instance_class: Object, class_class: Module)
@self_class = self_class
@instance_class = instance_class
@class_class = class_class
def self.warn_legacy_classes
return if @legacy_classes_warned
@legacy_classes_warned = true

Kernel.warn(
"`RBS::Test::TypeCheck.new` with `self_class:`, `instance_class:`, or `class_class:` is deprecated. Pass `context:` with a `RBS::Test::TypeCheck::InstanceContext` or `SingletonContext` instead.",
uplevel: 2
)
end

def initialize(builder:, sample_size:, unchecked_classes:, context: nil, owner: nil, self_class: nil, instance_class: nil, class_class: nil)
if self_class || instance_class || class_class
if context
raise ArgumentError, "`context:` cannot be given with `self_class:`, `instance_class:`, or `class_class:`"
end

TypeCheck.warn_legacy_classes
context = LegacyContext.new(
self_class: self_class,
instance_class: instance_class || Object,
class_class: class_class || Module
)
end

@context = context
@owner = owner || context&.self_class
@builder = builder
@sample_size = sample_size
@unchecked_classes = unchecked_classes.uniq
end

def self_class
context&.self_class
end

def instance_class
context&.instance_class
end

def class_class
context&.class_class
end

def overloaded_call(method, method_name, call, errors:)
es = method.defs.map do |type_def|
es = method_call(method_name, type_def.type, call, errors: [], annotations: type_def.annotations)
Expand All @@ -36,7 +117,7 @@ def overloaded_call(method, method_name, call, errors:)
errors.push(*es[0])
else
error = Errors::UnresolvedOverloadingError.new(
klass: self_class,
klass: owner,
method_name: method_name,
method_types: method.method_types
)
Expand Down Expand Up @@ -74,14 +155,14 @@ def method_call(method_name, method_type, call, errors:, annotations: [])
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)
errors << Errors::MissingBlockError.new(klass: owner, 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)
errors << Errors::UnexpectedBlockError.new(klass: owner, method_name: method_name, method_type: method_type)
end
end

Expand All @@ -91,7 +172,7 @@ def method_call(method_name, method_type, call, errors:, annotations: [])
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,
errors << type_error.new(klass: owner,
method_name: method_name,
method_type: method_type,
param: param,
Expand All @@ -100,7 +181,7 @@ def args(method_name, method_type, fun, call, errors, type_error:, argument_erro
end

unless test
errors << argument_error.new(klass: self_class,
errors << argument_error.new(klass: owner,
method_name: method_name,
method_type: method_type)
end
Expand All @@ -111,7 +192,7 @@ def return(method_name, method_type, fun, call, errors, return_error:, annotatio
return if Test.call(call.return_value, IS_AP, NilClass) && annotations.find { |a| a.string == "implicitly-returns-nil" }

unless value(call.return_value, fun.return_type)
errors << return_error.new(klass: self_class,
errors << return_error.new(klass: owner,
method_name: method_name,
method_type: method_type,
type: fun.return_type,
Expand Down Expand Up @@ -253,13 +334,13 @@ def value(val, type)
when Types::Bases::Void
true
when Types::Bases::Self
Test.call(val, IS_AP, self_class)
Test.call(val, IS_AP, self_class || raise(NoReceiverContextError.new(type)))
when Types::Bases::Nil
Test.call(val, IS_AP, ::NilClass)
when Types::Bases::Class
Test.call(val, IS_AP, class_class)
Test.call(val, IS_AP, class_class || raise(NoReceiverContextError.new(type)))
when Types::Bases::Instance
Test.call(val, IS_AP, instance_class)
Test.call(val, IS_AP, instance_class || raise(NoReceiverContextError.new(type)))
when Types::ClassInstance
klass = get_class(type.name) or return false
if params = builder.env.normalized_module_class_entry(type.name.absolute!)&.type_params
Expand Down
67 changes: 14 additions & 53 deletions lib/rbs/unit_test/type_assertions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,22 +109,18 @@ def testing(type_or_string)
end
end

def instance_class
def receiver_typecheck(receiver)
type, _ = target

case type
when RBS::Types::ClassSingleton, RBS::Types::ClassInstance
Object.const_get(type.name.to_s)
end
end

def class_class
type, _ = target
context =
case type
when RBS::Types::ClassSingleton
RBS::Test::TypeCheck::SingletonContext.of(receiver)
else
RBS::Test::TypeCheck::InstanceContext.of(receiver)
end

case type
when RBS::Types::ClassSingleton, RBS::Types::ClassInstance
Object.const_get(type.name.to_s).singleton_class
end
RBS::Test::TypeCheck.new(context: context, builder: builder, sample_size: 100, unchecked_classes: [])
end

def send_setup(method_type, receiver, method, args, proc)
Expand Down Expand Up @@ -170,14 +166,7 @@ def send_setup(method_type, receiver, method, args, proc)

ruby2_keywords def assert_send_type(method_type, receiver, method, *args, &block)
send_setup(method_type, receiver, method, args, block) do |method_type, trace, result, exception|
typecheck = RBS::Test::TypeCheck.new(
self_class: receiver.class,
builder: builder,
sample_size: 100,
unchecked_classes: [],
instance_class: instance_class,
class_class: class_class
)
typecheck = receiver_typecheck(receiver)
errors = typecheck.method_call(method, method_type, trace, errors: [])

assert_empty errors.map {|x| RBS::Test::Errors.to_string(x) }, -> { "Call trace does not match with given method type: #{trace.inspect}" }
Expand All @@ -194,14 +183,7 @@ def send_setup(method_type, receiver, method, args, proc)

ruby2_keywords def assert_send_type_error(method_type, error_type, receiver, method, *args, &block)
send_setup(method_type, receiver, method, args, block) do |method_type, trace, result, exception|
typecheck = RBS::Test::TypeCheck.new(
self_class: receiver.class,
builder: builder,
sample_size: 100,
unchecked_classes: [],
instance_class: instance_class,
class_class: class_class
)
typecheck = receiver_typecheck(receiver)
errors = typecheck.method_call(method, method_type, trace, errors: [])

assert_empty errors.map {|x| RBS::Test::Errors.to_string(x) }, -> { "Call trace does not match with given method type: #{trace.inspect}" }
Expand Down Expand Up @@ -231,14 +213,7 @@ def send_setup(method_type, receiver, method, args, proc)
type: method_type.type.with_return_type(RBS::Types::Bases::Any.new(location: nil))
)

typecheck = RBS::Test::TypeCheck.new(
self_class: receiver.class,
instance_class: instance_class,
class_class: class_class,
builder: builder,
sample_size: 100,
unchecked_classes: []
)
typecheck = receiver_typecheck(receiver)
errors = typecheck.method_call(method, method_type, trace, errors: [])

assert_operator exception, :is_a?, ::Exception
Expand Down Expand Up @@ -285,14 +260,7 @@ def allows_error(*errors)
def assert_const_type(type, constant_name)
constant = Object.const_get(constant_name)

typecheck = RBS::Test::TypeCheck.new(
self_class: constant.class,
instance_class: instance_class,
class_class: class_class,
builder: builder,
sample_size: 100,
unchecked_classes: []
)
typecheck = RBS::Test::TypeCheck.new(builder: builder, sample_size: 100, unchecked_classes: [])

value_type =
case type
Expand Down Expand Up @@ -333,14 +301,7 @@ def assert_visibility(visibility, method)
end

def assert_type(type, value)
typecheck = RBS::Test::TypeCheck.new(
self_class: value.class,
instance_class: _ = "No `instance` class allowed",
class_class: _ = "No `class` class allowed",
builder: builder,
sample_size: 100,
unchecked_classes: []
)
typecheck = RBS::Test::TypeCheck.new(builder: builder, sample_size: 100, unchecked_classes: [])

type =
case type
Expand Down
Loading
Loading