Skip to content

Commit 62c54e5

Browse files
committed
Add support for :through associations
1 parent 7a63050 commit 62c54e5

File tree

7 files changed

+90
-3
lines changed

7 files changed

+90
-3
lines changed

lib/ruby_lsp/ruby_lsp_rails/definition.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,23 @@ def handle_association(node)
108108
return unless first_argument.is_a?(Prism::SymbolNode)
109109

110110
association_name = first_argument.unescaped
111+
handle_association_name(association_name)
112+
113+
through_association_name = node.arguments.arguments
114+
.filter_map { |arg| arg.elements if arg.is_a?(Prism::KeywordHashNode) }
115+
.flatten
116+
.find { |elem| elem.key.value == "through" }
117+
&.value
118+
&.unescaped
119+
handle_association_name(through_association_name) if through_association_name
120+
end
111121

122+
#: (String association_name) -> void
123+
def handle_association_name(association_name)
112124
result = @client.association_target_location(
113125
model_name: @nesting.join("::"),
114126
association_name: association_name,
115127
)
116-
117128
return unless result
118129

119130
@response_builder << Support::LocationBuilder.line_location_from_s(result.fetch(:location))

test/dummy/app/models/country.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# frozen_string_literal: true
22

33
class Country < ApplicationRecord
4+
has_one :flag, dependent: :destroy
45
end

test/dummy/app/models/flag.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# frozen_string_literal: true
2+
3+
class Flag < ApplicationRecord
4+
belongs_to :country
5+
end

test/dummy/app/models/user.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ class User < ApplicationRecord
55
validates :first_name, presence: true
66
has_one :profile
77
scope :adult, -> { where(age: 18..) }
8-
has_one :location, class_name: "Country"
8+
belongs_to :location, class_name: "Country"
9+
has_one :country_flag, through: :location, source: :flag
910

1011
attr_readonly :last_name
1112

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class CreateFlags < ActiveRecord::Migration[8.0]
2+
def change
3+
create_table :flags do |t|
4+
t.references :country, null: false, foreign_key: true
5+
6+
t.timestamps
7+
end
8+
end
9+
end

test/dummy/db/schema.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[8.0].define(version: 2024_10_25_225348) do
13+
ActiveRecord::Schema[8.0].define(version: 2025_07_03_132109) do
1414
create_table "composite_primary_keys", primary_key: ["order_id", "product_id"], force: :cascade do |t|
1515
t.integer "order_id"
1616
t.integer "product_id"
@@ -25,6 +25,13 @@
2525
t.datetime "updated_at", null: false
2626
end
2727

28+
create_table "flags", force: :cascade do |t|
29+
t.integer "country_id", null: false
30+
t.datetime "created_at", null: false
31+
t.datetime "updated_at", null: false
32+
t.index ["country_id"], name: "index_flags_on_country_id"
33+
end
34+
2835
create_table "memberships", force: :cascade do |t|
2936
t.integer "user_id", null: false
3037
t.integer "organization_id", null: false
@@ -58,6 +65,7 @@
5865
t.index ["country_id"], name: "index_users_on_country_id"
5966
end
6067

68+
add_foreign_key "flags", "countries"
6169
add_foreign_key "memberships", "organizations"
6270
add_foreign_key "memberships", "users"
6371
add_foreign_key "users", "countries"

test/ruby_lsp_rails/definition_test.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,32 @@ class Organization < ActiveRecord::Base
5252
assert_equal(2, response[0].range.end.line)
5353
end
5454

55+
test "recognizes has_many :through model associations" do
56+
response = generate_definitions_for_source(<<~RUBY, { line: 4, character: 29 })
57+
# typed: false
58+
59+
class Organization < ActiveRecord::Base
60+
has_many :memberships
61+
has_many :users, through: :memberships
62+
end
63+
RUBY
64+
65+
assert_equal(2, response.size)
66+
67+
assert_equal(
68+
URI::Generic.from_path(path: File.join(dummy_root, "app", "models", "user.rb")).to_s,
69+
response[0].uri,
70+
)
71+
assert_equal(
72+
URI::Generic.from_path(path: File.join(dummy_root, "app", "models", "membership.rb")).to_s,
73+
response[1].uri,
74+
)
75+
assert_equal(2, response[0].range.start.line)
76+
assert_equal(2, response[0].range.end.line)
77+
assert_equal(2, response[1].range.start.line)
78+
assert_equal(2, response[1].range.end.line)
79+
end
80+
5581
test "recognizes belongs_to model associations" do
5682
response = generate_definitions_for_source(<<~RUBY, { line: 3, character: 14 })
5783
# typed: false
@@ -90,6 +116,32 @@ class User < ActiveRecord::Base
90116
assert_equal(2, response[0].range.end.line)
91117
end
92118

119+
test "recognizes has_one :through model associations" do
120+
response = generate_definitions_for_source(<<~RUBY, { line: 4, character: 35 })
121+
# typed: false
122+
123+
class User < ActiveRecord::Base
124+
belongs_to :location, class_name: "Country"
125+
has_one :country_flag, through: :location, source: :flag
126+
end
127+
RUBY
128+
129+
assert_equal(2, response.size)
130+
131+
assert_equal(
132+
URI::Generic.from_path(path: File.join(dummy_root, "app", "models", "flag.rb")).to_s,
133+
response[0].uri,
134+
)
135+
assert_equal(
136+
URI::Generic.from_path(path: File.join(dummy_root, "app", "models", "country.rb")).to_s,
137+
response[1].uri,
138+
)
139+
assert_equal(2, response[0].range.start.line)
140+
assert_equal(2, response[0].range.end.line)
141+
assert_equal(2, response[1].range.start.line)
142+
assert_equal(2, response[1].range.end.line)
143+
end
144+
93145
test "recognizes has_and_belongs_to_many model associations" do
94146
response = generate_definitions_for_source(<<~RUBY, { line: 3, character: 27 })
95147
# typed: false

0 commit comments

Comments
 (0)