From 5c03ec96525c3a0e5d37f9854aac8cdf03311dd7 Mon Sep 17 00:00:00 2001 From: akitoshiga Date: Thu, 22 Oct 2026 16:03:55 +0900 Subject: [PATCH] Add types for methods ActiveRecord::Relation delegates to records ActiveRecord::Relation delegates Array methods such as length, [], join and sample to records. The generated RBS defines them in ActiveRecord::Delegation as untyped, so the element type of the relation is lost and callers have to insert to_a to get typed values. Define them in an interface and include it into ActiveRecord::Relation::Methods so that they are typed with the model of each relation. Co-authored-by: Cursor --- .../_test/activerecord-relation-delegation.rb | 62 +++++++++++++++++++ gems/activerecord/8.1/activerecord.rbs | 46 ++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 gems/activerecord/8.1/_test/activerecord-relation-delegation.rb diff --git a/gems/activerecord/8.1/_test/activerecord-relation-delegation.rb b/gems/activerecord/8.1/_test/activerecord-relation-delegation.rb new file mode 100644 index 00000000..9b81ee6f --- /dev/null +++ b/gems/activerecord/8.1/_test/activerecord-relation-delegation.rb @@ -0,0 +1,62 @@ +module Test + users = User.all + + # @type var to_xml: String + to_xml = users.to_xml + # @type var length: Integer + length = users.length + # @type var joined: String + joined = users.join(", ") + # @type var intersected: bool + intersected = users.intersect?(User.all) + # @type var user_at: User? + user_at = users[0] + # @type var users_in_range: Array[User]? + users_in_range = users[0..1] + # @type var users_from_start: Array[User]? + users_from_start = users[0, 2] + # @type var and_users: Array[User] + and_users = users & User.all + # @type var or_users: Array[User | Integer] + or_users = users | [1] + # @type var plus_users: Array[User | Integer] + plus_users = users + [1] + # @type var minus_users: Array[User] + minus_users = users - User.all + # @type var sampled_user: User? + sampled_user = users.sample + # @type var sampled_users: Array[User] + sampled_users = users.sample(2) + # @type var reversed: Array[User] + reversed = users.reverse + # @type var rotated: Array[User] + rotated = users.rotate(1) + # @type var compacted: Array[User] + compacted = users.compact + # @type var groups: Array[Array[User?]] + groups = users.in_groups(2) + # @type var groups_with_filler: Array[Array[User | String]] + groups_with_filler = users.in_groups_of(2, "") + users.in_groups_of(2) { |group| group.compact.each(&:save!) } + # @type var sentence: String + sentence = users.to_sentence + # @type var formatted: String + formatted = users.to_fs(:db) + # @type var formatted_s: String + formatted_s = users.to_formatted_s + # @type var json: Array[untyped] + json = users.as_json + # @type var shuffled: Array[User] + shuffled = users.shuffle + # @type var splitted: Array[Array[User]] + splitted = users.split { |user| user.persisted? } + # @type var sliced: Array[User]? + sliced = users.slice(0, 2) + # @type var found_index: Integer? + found_index = users.index { |user| user.persisted? } + # @type var found_rindex: Integer? + found_rindex = users.rindex(users.first) + + # @type var article: Article? + article = User.take.articles[0] +end diff --git a/gems/activerecord/8.1/activerecord.rbs b/gems/activerecord/8.1/activerecord.rbs index 05851655..1954368f 100644 --- a/gems/activerecord/8.1/activerecord.rbs +++ b/gems/activerecord/8.1/activerecord.rbs @@ -362,12 +362,58 @@ end module ActiveRecord module Delegation def table_name: () -> String + + # Methods delegated to `records` by ActiveRecord::Delegation. + # They are typed here with the model type because the generated definitions in Delegation are untyped. + # RBS has no way to declare delegation or to reuse only some method types of `Array[Model]`, + # so the signatures are copied from `Array` in core RBS and activesupport. + interface _DelegateToRecords[Model] + def to_xml: (?Hash[untyped, untyped] options) ?{ (untyped) -> untyped } -> String + def encode_with: (untyped coder) -> void + def length: () -> Integer + def join: (?string? separator) -> String + def intersect?: (array[untyped] other) -> bool + def []: (int index) -> Model? + | (int start, int length) -> Array[Model]? + | (range[int] range) -> Array[Model]? + def &: (array[untyped] other) -> Array[Model] + def |: [T] (array[T] other) -> Array[Model | T] + def +: [T] (array[T] other) -> Array[Model | T] + def -: (array[untyped] other) -> Array[Model] + def sample: (?random: ::Array::_Rand) -> Model? + | (int n, ?random: ::Array::_Rand) -> Array[Model] + def reverse: () -> Array[Model] + def rotate: (?int count) -> Array[Model] + def compact: () -> Array[Model] + def in_groups: (Integer number) ?{ (Array[Model?]) -> void } -> Array[Array[Model?]] + | [T] (Integer number, T fill_with) ?{ (Array[Model | T]) -> void } -> Array[Array[Model | T]] + def in_groups_of: (Integer number) ?{ (Array[Model?]) -> void } -> Array[Array[Model?]] + | [T] (Integer number, T fill_with) ?{ (Array[Model | T]) -> void } -> Array[Array[Model | T]] + def to_sentence: (?Hash[Symbol, untyped] options) -> String + def to_fs: (?Symbol format) -> String + def to_formatted_s: (?Symbol format) -> String + def as_json: (?untyped options) -> Array[untyped] + def shuffle: (?random: ::Array::_Rand) -> Array[Model] + def split: (?untyped value) -> Array[Array[Model]] + | () { (Model) -> boolish } -> Array[Array[Model]] + def slice: (int index) -> Model? + | (int start, int length) -> Array[Model]? + | (range[int] range) -> Array[Model]? + def index: (untyped obj) -> Integer? + | () { (Model) -> boolish } -> Integer? + | () -> Enumerator[Model, Integer?] + def rindex: (untyped obj) -> Integer? + | () { (Model) -> boolish } -> Integer? + | () -> Enumerator[Model, Integer?] + end end end module ActiveRecord class Relation module Methods[Model, PrimaryKey] + include ::ActiveRecord::Delegation::_DelegateToRecords[Model] + def all: () -> self def ids: () -> Array[PrimaryKey] def none: () -> self