Skip to content

Implement document symbols for validations #283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 65 additions & 19 deletions lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,25 +93,29 @@ def on_call_node_enter(node)
)
end

extract_callbacks(node)
end

private

sig { params(node: Prism::CallNode).void }
def extract_callbacks(node)
receiver = node.receiver
return if receiver && !receiver.is_a?(Prism::SelfNode)

message_value = node.message
message = node.message
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
message = node.message
message = T.must(node.message)

then we can remove the T.musts below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this one.

node.message is nilable. Once we enter the switch-case, we know for sure that it's not nil, but I don't think we can guarantee that before entering.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged it without this, but we can fix this in the next PR based on your thoughts.

case message
when *CALLBACKS, "validate"
handle_all_arg_types(node, T.must(message))
when "validates", "validates!", "validates_each"
handle_symbol_and_string_arg_types(node, T.must(message))
when "validates_with"
handle_class_arg_types(node, T.must(message))
end
end

return unless CALLBACKS.include?(message_value)
private

sig { params(node: Prism::CallNode, message: String).void }
def handle_all_arg_types(node, message)
block = node.block

if block
append_document_symbol(
name: "#{message_value}(<anonymous>)",
name: "#{message}(<anonymous>)",
range: range_from_location(node.location),
selection_range: range_from_location(block.location),
)
Expand All @@ -128,7 +132,7 @@ def extract_callbacks(node)
next unless name

append_document_symbol(
name: "#{message_value}(#{name})",
name: "#{message}(#{name})",
range: range_from_location(argument.location),
selection_range: range_from_location(T.must(argument.value_loc)),
)
Expand All @@ -137,43 +141,85 @@ def extract_callbacks(node)
next if name.empty?

append_document_symbol(
name: "#{message_value}(#{name})",
name: "#{message}(#{name})",
range: range_from_location(argument.location),
selection_range: range_from_location(argument.content_loc),
)
when Prism::LambdaNode
append_document_symbol(
name: "#{message_value}(<anonymous>)",
name: "#{message}(<anonymous>)",
range: range_from_location(node.location),
selection_range: range_from_location(argument.location),
)
when Prism::CallNode
next unless argument.name == :new

arg_receiver = argument.receiver

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

append_document_symbol(
name: "#{message_value}(#{name})",
name: "#{message}(#{name})",
range: range_from_location(argument.location),
selection_range: range_from_location(argument.location),
)
when Prism::ConstantReadNode
name = argument.name
when Prism::ConstantReadNode, Prism::ConstantPathNode
name = argument.full_name
next if name.empty?

append_document_symbol(
name: "#{message_value}(#{name})",
name: "#{message}(#{name})",
range: range_from_location(argument.location),
selection_range: range_from_location(argument.location),
)
when Prism::ConstantPathNode
end
end
end

sig { params(node: Prism::CallNode, message: String).void }
def handle_symbol_and_string_arg_types(node, message)
arguments = node.arguments&.arguments
return unless arguments&.any?

arguments.each do |argument|
case argument
when Prism::SymbolNode
name = argument.value
next unless name

append_document_symbol(
name: "#{message}(#{name})",
range: range_from_location(argument.location),
selection_range: range_from_location(T.must(argument.value_loc)),
)
when Prism::StringNode
name = argument.content
next if name.empty?

append_document_symbol(
name: "#{message}(#{name})",
range: range_from_location(argument.location),
selection_range: range_from_location(argument.content_loc),
)
end
end
end

sig { params(node: Prism::CallNode, message: String).void }
def handle_class_arg_types(node, message)
arguments = node.arguments&.arguments
return unless arguments&.any?

arguments.each do |argument|
case argument
when Prism::ConstantReadNode, Prism::ConstantPathNode
name = argument.full_name
next if name.empty?

append_document_symbol(
name: "#{message_value}(#{name})",
name: "#{message}(#{name})",
range: range_from_location(argument.location),
selection_range: range_from_location(argument.location),
)
Expand Down
1 change: 1 addition & 0 deletions test/dummy/app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

class User < ApplicationRecord
before_create :foo_arg, -> () {}
validates :name, presence: true
end
74 changes: 68 additions & 6 deletions test/ruby_lsp_rails/document_symbol_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ class NestedTest < ActiveSupport::TestCase
assert_equal("back to the same level", response[0].children[2].name)
end

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

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

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

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

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

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

test "correctly handles validate method with all argument types" do
response = generate_document_symbols_for_source(<<~RUBY)
class FooModel < ApplicationRecord
validate "foo_arg", :bar_arg, -> () {}, Foo::BazClass.new("blah"), FooClass, Foo::BarClass
end
RUBY

assert_equal(1, response.size)
assert_equal("FooModel", response[0].name)
assert_equal(6, response[0].children.size)
assert_equal("validate(foo_arg)", response[0].children[0].name)
assert_equal("validate(bar_arg)", response[0].children[1].name)
assert_equal("validate(<anonymous>)", response[0].children[2].name)
assert_equal("validate(Foo::BazClass)", response[0].children[3].name)
assert_equal("validate(FooClass)", response[0].children[4].name)
assert_equal("validate(Foo::BarClass)", response[0].children[5].name)
end

test "correctly handles validates method with string and symbol argument types" do
response = generate_document_symbols_for_source(<<~RUBY)
class FooModel < ApplicationRecord
validates "foo_arg", :bar_arg
end
RUBY

assert_equal(1, response.size)
assert_equal("FooModel", response[0].name)
assert_equal(2, response[0].children.size)
assert_equal("validates(foo_arg)", response[0].children[0].name)
assert_equal("validates(bar_arg)", response[0].children[1].name)
end

test "correctly handles validates_each method with string and symbol argument types" do
response = generate_document_symbols_for_source(<<~RUBY)
class FooModel < ApplicationRecord
validates_each "foo_arg", :bar_arg do
puts "Foo"
end
end
RUBY

assert_equal(1, response.size)
assert_equal("FooModel", response[0].name)
assert_equal(2, response[0].children.size)
assert_equal("validates_each(foo_arg)", response[0].children[0].name)
assert_equal("validates_each(bar_arg)", response[0].children[1].name)
end

test "correctly handles validates_with method with constant and namespaced constant argument types" do
response = generate_document_symbols_for_source(<<~RUBY)
class FooModel < ApplicationRecord
validates_with FooClass, Foo::BarClass
end
RUBY

assert_equal(1, response.size)
assert_equal("FooModel", response[0].name)
assert_equal(2, response[0].children.size)
assert_equal("validates_with(FooClass)", response[0].children[0].name)
assert_equal("validates_with(Foo::BarClass)", response[0].children[1].name)
end

private

def generate_document_symbols_for_source(source)
Expand Down