Skip to content

Resolve self, instance, and class from the receiver in runtime type checks - #3194

Merged
ksss merged 3 commits into
masterfrom
receiver-context-type-check
Oct 5, 2026
Merged

ksss merged 3 commits into
masterfrom
receiver-context-type-check

Conversation

@ksss

@ksss ksss commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #3192.

RBS::Test::TypeCheck received the classes for self, instance, and class as three separate arguments, and its callers filled them in inconsistently. As a result, a method called on a subclass could not be checked precisely, and rbs test did not check instance at all.

At runtime, all three are determined by the receiver's class and whether the call is an instance or a singleton method call. This PR models that as a single value:

TypeCheck.new(context: TypeCheck::InstanceContext.of(receiver), builder:, sample_size:, unchecked_classes:)
TypeCheck.new(context: TypeCheck::SingletonContext.of(receiver), builder:, sample_size:, unchecked_classes:)
self instance class
InstanceContext.of(obj) obj.class obj.class obj.class.singleton_class
SingletonContext.of(mod) mod.singleton_class mod mod.singleton_class

Both rbs test and RBS::UnitTest::TypeAssertions now build the context from the actual receiver. In rbs test, a module's instance method called on a class that extends the module is checked as a singleton method call, as Steep does.

The subject of the errors is given separately as owner:. rbs test passes the hooked class, so error tags stay the same as before. With the reproduction from the issue, rbs test now reports all four violations:

[Foo.make] ReturnTypeError: expected `instance` but returns `#<Foo>`
[Foo#copy] ReturnTypeError: expected `self` but returns `#<Foo>`
[Foo.bad_instance] ReturnTypeError: expected `instance` but returns `1`
[Foo#bad_self] ReturnTypeError: expected `self` but returns `1`

Compatibility

  • self_class:, instance_class:, and class_class: are still accepted, with a deprecation warning shown once per process. Passing them together with context: raises ArgumentError.
  • Without a context, checking self, instance, or class raises TypeCheck::NoReceiverContextError. assert_type and assert_const_type use this instead of placeholder values.
  • TypeAssertions#instance_class and #class_class are removed.

🤖 Generated with Claude Code

…type checks

`RBS::Test::TypeCheck` took the classes for `self`, `instance`, and
`class` as three independent arguments, and each caller filled them in
differently. `assert_send_type` took `instance` from the `testing`
target, and `rbs test` checked `self` against the hooked class and left
`instance` as `Object`. So a method called on a subclass couldn't be
checked against `instance` or `self`, and `rbs test` didn't check
`instance` at all.

At runtime the three types are determined by the receiver's class and
whether the call is an instance or a singleton method call: `instance`
is the class, `class` is its singleton class, and `self` is one of
them. Model that as `TypeCheck::InstanceContext` and
`TypeCheck::SingletonContext`, built from the receiver with `.of`, and
pass one of them as `context:`. Both `rbs test` and the unit test
assertions build the context from the actual receiver.

Without a context, checking `self`, `instance`, or `class` raises
`TypeCheck::NoReceiverContextError`, which replaces the placeholder
strings `assert_type` used to pass. The `self_class:`,
`instance_class:`, and `class_class:` keywords still work but emit a
deprecation warning.

Fixes #3192

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for the receiver context:

- A module's instance method hook also runs when the module is used via
  `extend`, where the receiver is a class. Building an `InstanceContext`
  from it made `instance` mean `Class`, so `Widget.build` with
  `def build: () -> instance` was reported as an error. When the
  receiver is a module whose class doesn't include the hooked module,
  use a `SingletonContext` instead, as Steep does.

- Errors were tagged with the receiver's class, which loses the module
  that declares the method (`[C#foo]` instead of `[M#foo]`) and drops
  the name of anonymous subclasses. Add `owner:` to `TypeCheck.new` for
  the subject of the errors. `rbs test` passes the hooked class, so the
  tags are the same as before.

- Passing `context:` together with the deprecated keywords now raises
  `ArgumentError` instead of ignoring them. The deprecation warning is
  shown without the `:deprecated` category, like the other deprecation
  in this gem, and only once per process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@soutaro soutaro self-assigned this Oct 5, 2026
@ksss
ksss added this pull request to the merge queue Oct 5, 2026
Merged via the queue into master with commit 02de16f Oct 5, 2026
24 checks passed
@ksss
ksss deleted the receiver-context-type-check branch October 5, 2026 08:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RBS::Test / assert_send_type can't check instance and self precisely for subclass receivers

2 participants