Skip to content

alba: bind class_eval'd DSL blocks to T.self_type - #424

Merged
amomchilov merged 2 commits into
Shopify:mainfrom
mokevnin:alba-nested-block-self-type
Sep 23, 2026
Merged

amomchilov merged 2 commits into
Shopify:mainfrom
mokevnin:alba-nested-block-self-type

Conversation

@mokevnin

Copy link
Copy Markdown
Contributor

Type of Change

  • Modify RBI for an existing gem

Changes

Follow-up to #422, which is mine: I typed the blocks of nested, nested_attribute and trait as T.proc.bind(T.untyped). That binding is weaker than the runtime warrants, and the erasure it causes is not local to the block's own self.

All three class_eval the block on a subclass of the resource class the macro was called on:

# lib/alba/nested_attribute.rb
resource_class = Class.new(@klass)
resource_class.class_eval(&@block)

# lib/alba/resource.rb, #hash_from_traits
resource_class = Class.new(self.class)
resource_class.class_eval(&body)

So self inside the block is exactly the receiver of the macro, which T.self_type names precisely. This differs from the ActiveSupport::Concern case in #423: a concern genuinely cannot know its includer, whereas here the type is right there in the call.

The cost of T.untyped is that it also erases what is declared inside the block. A project that types its resources (a Tapioca DSL compiler generating a per-resource attribute sig, or a hand-written shim) loses that typing for everything inside nested do ... end — the block parameter, and every value derived from it, silently becomes untyped, because attribute is now being called on an untyped self. In our app this turned a correct T.must(it.price) on a T.nilable(SubscriptionPrice) into a 7050 "redundant T.must", i.e. the annotation did not just fail to add information, it removed information Sorbet previously had. With T.self_type that typing is preserved, and inside the block plain nested/attribute calls still resolve through Alba::Resource::ClassMethods as before.

repo check index, rubocop alba, runtime alba and static alba all pass locally.

Blocks left alone deliberately: attribute, meta, on_nil and the association source blocks are instance_exec'd on a resource instance, not the class, so T.self_type would be wrong there and the honest type is a separate question.

🤖 Generated with Claude Code

`nested`, `nested_attribute` and `trait` class_eval their block on a
subclass of the resource the macro was called on, so `self` inside the
block is that resource class — a type the sig can name. `T.untyped`
erases it, and with it every `attribute` declared inside the block: a
DSL-typing RBI for the resource class no longer applies, so the block
parameter and anything derived from it fall back to untyped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mokevnin
mokevnin requested a review from a team as a code owner September 13, 2026 16:25
@amomchilov amomchilov self-assigned this Sep 17, 2026
@amomchilov
amomchilov merged commit 80d9a76 into Shopify:main Sep 23, 2026
4 checks passed
@mokevnin

Copy link
Copy Markdown
Contributor Author

Thanks for the quick review and merge!

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.

3 participants