Skip to content

Commit d1ed636

Browse files
authored
Merge pull request #283 from Shopify/as/document-symbol-validations
Implement document symbols for validations
2 parents cbcf869 + d349619 commit d1ed636

File tree

3 files changed

+134
-25
lines changed

3 files changed

+134
-25
lines changed

lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -93,25 +93,29 @@ def on_call_node_enter(node)
9393
)
9494
end
9595

96-
extract_callbacks(node)
97-
end
98-
99-
private
100-
101-
sig { params(node: Prism::CallNode).void }
102-
def extract_callbacks(node)
10396
receiver = node.receiver
10497
return if receiver && !receiver.is_a?(Prism::SelfNode)
10598

106-
message_value = node.message
99+
message = node.message
100+
case message
101+
when *CALLBACKS, "validate"
102+
handle_all_arg_types(node, T.must(message))
103+
when "validates", "validates!", "validates_each"
104+
handle_symbol_and_string_arg_types(node, T.must(message))
105+
when "validates_with"
106+
handle_class_arg_types(node, T.must(message))
107+
end
108+
end
107109

108-
return unless CALLBACKS.include?(message_value)
110+
private
109111

112+
sig { params(node: Prism::CallNode, message: String).void }
113+
def handle_all_arg_types(node, message)
110114
block = node.block
111115

112116
if block
113117
append_document_symbol(
114-
name: "#{message_value}(<anonymous>)",
118+
name: "#{message}(<anonymous>)",
115119
range: range_from_location(node.location),
116120
selection_range: range_from_location(block.location),
117121
)
@@ -128,7 +132,7 @@ def extract_callbacks(node)
128132
next unless name
129133

130134
append_document_symbol(
131-
name: "#{message_value}(#{name})",
135+
name: "#{message}(#{name})",
132136
range: range_from_location(argument.location),
133137
selection_range: range_from_location(T.must(argument.value_loc)),
134138
)
@@ -137,43 +141,85 @@ def extract_callbacks(node)
137141
next if name.empty?
138142

139143
append_document_symbol(
140-
name: "#{message_value}(#{name})",
144+
name: "#{message}(#{name})",
141145
range: range_from_location(argument.location),
142146
selection_range: range_from_location(argument.content_loc),
143147
)
144148
when Prism::LambdaNode
145149
append_document_symbol(
146-
name: "#{message_value}(<anonymous>)",
150+
name: "#{message}(<anonymous>)",
147151
range: range_from_location(node.location),
148152
selection_range: range_from_location(argument.location),
149153
)
150154
when Prism::CallNode
155+
next unless argument.name == :new
156+
151157
arg_receiver = argument.receiver
152158

153159
name = arg_receiver.name if arg_receiver.is_a?(Prism::ConstantReadNode)
154160
name = arg_receiver.full_name if arg_receiver.is_a?(Prism::ConstantPathNode)
155161
next unless name
156162

157163
append_document_symbol(
158-
name: "#{message_value}(#{name})",
164+
name: "#{message}(#{name})",
159165
range: range_from_location(argument.location),
160166
selection_range: range_from_location(argument.location),
161167
)
162-
when Prism::ConstantReadNode
163-
name = argument.name
168+
when Prism::ConstantReadNode, Prism::ConstantPathNode
169+
name = argument.full_name
164170
next if name.empty?
165171

166172
append_document_symbol(
167-
name: "#{message_value}(#{name})",
173+
name: "#{message}(#{name})",
168174
range: range_from_location(argument.location),
169175
selection_range: range_from_location(argument.location),
170176
)
171-
when Prism::ConstantPathNode
177+
end
178+
end
179+
end
180+
181+
sig { params(node: Prism::CallNode, message: String).void }
182+
def handle_symbol_and_string_arg_types(node, message)
183+
arguments = node.arguments&.arguments
184+
return unless arguments&.any?
185+
186+
arguments.each do |argument|
187+
case argument
188+
when Prism::SymbolNode
189+
name = argument.value
190+
next unless name
191+
192+
append_document_symbol(
193+
name: "#{message}(#{name})",
194+
range: range_from_location(argument.location),
195+
selection_range: range_from_location(T.must(argument.value_loc)),
196+
)
197+
when Prism::StringNode
198+
name = argument.content
199+
next if name.empty?
200+
201+
append_document_symbol(
202+
name: "#{message}(#{name})",
203+
range: range_from_location(argument.location),
204+
selection_range: range_from_location(argument.content_loc),
205+
)
206+
end
207+
end
208+
end
209+
210+
sig { params(node: Prism::CallNode, message: String).void }
211+
def handle_class_arg_types(node, message)
212+
arguments = node.arguments&.arguments
213+
return unless arguments&.any?
214+
215+
arguments.each do |argument|
216+
case argument
217+
when Prism::ConstantReadNode, Prism::ConstantPathNode
172218
name = argument.full_name
173219
next if name.empty?
174220

175221
append_document_symbol(
176-
name: "#{message_value}(#{name})",
222+
name: "#{message}(#{name})",
177223
range: range_from_location(argument.location),
178224
selection_range: range_from_location(argument.location),
179225
)

test/dummy/app/models/user.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33

44
class User < ApplicationRecord
55
before_create :foo_arg, -> () {}
6+
validates :name, presence: true
67
end

test/ruby_lsp_rails/document_symbol_test.rb

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ class NestedTest < ActiveSupport::TestCase
163163
assert_equal("back to the same level", response[0].children[2].name)
164164
end
165165

166-
test "correctly handles model callbacks with multiple Prism::StringNode arguments" do
166+
test "correctly handles model callbacks with multiple string arguments" do
167167
response = generate_document_symbols_for_source(<<~RUBY)
168168
class FooModel < ApplicationRecord
169169
before_save "foo_method", "bar_method", on: :update
@@ -192,7 +192,7 @@ class FooController < ApplicationController
192192
assert_equal("before_action(<anonymous>)", response[0].children[0].name)
193193
end
194194

195-
test "correctly handles job callback with Prism::SymbolNode argument" do
195+
test "correctly handles job callback with symbol argument" do
196196
response = generate_document_symbols_for_source(<<~RUBY)
197197
class FooJob < ApplicationJob
198198
before_perform :foo_method
@@ -205,7 +205,7 @@ class FooJob < ApplicationJob
205205
assert_equal("before_perform(foo_method)", response[0].children[0].name)
206206
end
207207

208-
test "correctly handles model callback with Prism::LambdaNode argument" do
208+
test "correctly handles model callback with lambda argument" do
209209
response = generate_document_symbols_for_source(<<~RUBY)
210210
class FooModel < ApplicationRecord
211211
before_save -> () {}
@@ -218,7 +218,7 @@ class FooModel < ApplicationRecord
218218
assert_equal("before_save(<anonymous>)", response[0].children[0].name)
219219
end
220220

221-
test "correctly handles job callbacks with Prism::CallNode argument" do
221+
test "correctly handles job callbacks with method call argument" do
222222
response = generate_document_symbols_for_source(<<~RUBY)
223223
class FooJob < ApplicationJob
224224
before_perform FooClass.new(foo_arg)
@@ -231,7 +231,7 @@ class FooJob < ApplicationJob
231231
assert_equal("before_perform(FooClass)", response[0].children[0].name)
232232
end
233233

234-
test "correctly handles controller callbacks with Prism::ConstantReadNode argument" do
234+
test "correctly handles controller callbacks with constant argument" do
235235
response = generate_document_symbols_for_source(<<~RUBY)
236236
class FooController < ApplicationController
237237
before_action FooClass
@@ -244,7 +244,7 @@ class FooController < ApplicationController
244244
assert_equal("before_action(FooClass)", response[0].children[0].name)
245245
end
246246

247-
test "correctly handles model callbacks with Prism::ConstantPathNode argument" do
247+
test "correctly handles model callbacks with namespaced constant argument" do
248248
response = generate_document_symbols_for_source(<<~RUBY)
249249
class FooModel < ApplicationRecord
250250
before_save Foo::BarClass
@@ -287,6 +287,68 @@ class FooJob < ApplicationJob
287287
assert_empty(response[0].children)
288288
end
289289

290+
test "correctly handles validate method with all argument types" do
291+
response = generate_document_symbols_for_source(<<~RUBY)
292+
class FooModel < ApplicationRecord
293+
validate "foo_arg", :bar_arg, -> () {}, Foo::BazClass.new("blah"), FooClass, Foo::BarClass
294+
end
295+
RUBY
296+
297+
assert_equal(1, response.size)
298+
assert_equal("FooModel", response[0].name)
299+
assert_equal(6, response[0].children.size)
300+
assert_equal("validate(foo_arg)", response[0].children[0].name)
301+
assert_equal("validate(bar_arg)", response[0].children[1].name)
302+
assert_equal("validate(<anonymous>)", response[0].children[2].name)
303+
assert_equal("validate(Foo::BazClass)", response[0].children[3].name)
304+
assert_equal("validate(FooClass)", response[0].children[4].name)
305+
assert_equal("validate(Foo::BarClass)", response[0].children[5].name)
306+
end
307+
308+
test "correctly handles validates method with string and symbol argument types" do
309+
response = generate_document_symbols_for_source(<<~RUBY)
310+
class FooModel < ApplicationRecord
311+
validates "foo_arg", :bar_arg
312+
end
313+
RUBY
314+
315+
assert_equal(1, response.size)
316+
assert_equal("FooModel", response[0].name)
317+
assert_equal(2, response[0].children.size)
318+
assert_equal("validates(foo_arg)", response[0].children[0].name)
319+
assert_equal("validates(bar_arg)", response[0].children[1].name)
320+
end
321+
322+
test "correctly handles validates_each method with string and symbol argument types" do
323+
response = generate_document_symbols_for_source(<<~RUBY)
324+
class FooModel < ApplicationRecord
325+
validates_each "foo_arg", :bar_arg do
326+
puts "Foo"
327+
end
328+
end
329+
RUBY
330+
331+
assert_equal(1, response.size)
332+
assert_equal("FooModel", response[0].name)
333+
assert_equal(2, response[0].children.size)
334+
assert_equal("validates_each(foo_arg)", response[0].children[0].name)
335+
assert_equal("validates_each(bar_arg)", response[0].children[1].name)
336+
end
337+
338+
test "correctly handles validates_with method with constant and namespaced constant argument types" do
339+
response = generate_document_symbols_for_source(<<~RUBY)
340+
class FooModel < ApplicationRecord
341+
validates_with FooClass, Foo::BarClass
342+
end
343+
RUBY
344+
345+
assert_equal(1, response.size)
346+
assert_equal("FooModel", response[0].name)
347+
assert_equal(2, response[0].children.size)
348+
assert_equal("validates_with(FooClass)", response[0].children[0].name)
349+
assert_equal("validates_with(Foo::BarClass)", response[0].children[1].name)
350+
end
351+
290352
private
291353

292354
def generate_document_symbols_for_source(source)

0 commit comments

Comments
 (0)