From 5d10821f1fee1dde710076010d24383841820c3a Mon Sep 17 00:00:00 2001 From: Jose Camacho Date: Tue, 4 Nov 2025 14:11:40 -0600 Subject: [PATCH 1/5] Add documentSymbol support to schema.rb --- .../ruby_lsp_rails/document_symbol.rb | 22 ++++++++++++++ test/ruby_lsp_rails/document_symbol_test.rb | 30 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb b/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb index bdad1445..bff0c22f 100644 --- a/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb +++ b/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb @@ -24,6 +24,7 @@ def initialize(response_builder, dispatcher) :on_class_node_leave, :on_module_node_enter, :on_module_node_leave, + :on_constant_path_node_enter, ) end @@ -55,6 +56,8 @@ def on_call_node_enter(node) handle_symbol_and_string_arg_types(node, message) when "validates_with" handle_class_arg_types(node, message) + when "create_table" + handle_create_table(node) end end @@ -78,6 +81,14 @@ def on_module_node_leave(node) remove_from_namespace_stack(node) end + def on_constant_path_node_enter(node) + return if node.is_a?(Prism::SelfNode) || node.parent.is_a?(Prism::SelfNode) + + return unless node.parent.name == :ActiveRecord && node.name == :Schema + + @namespace_stack << node.full_name + end + private #: ((Prism::ClassNode | Prism::ModuleNode) node) -> void @@ -213,6 +224,17 @@ def handle_class_arg_types(node, message) end end + #: (Prism::CallNode node, String message) -> void + def handle_create_table(node) + table_name_argument = node.arguments.arguments.first + + append_document_symbol( + name: table_name_argument.content, + range: range_from_location(table_name_argument.location), + selection_range: range_from_location(table_name_argument.content_loc), + ) + end + #: (name: String, range: RubyLsp::Interface::Range, selection_range: RubyLsp::Interface::Range) -> void def append_document_symbol(name:, range:, selection_range:) @response_builder.last.children << RubyLsp::Interface::DocumentSymbol.new( diff --git a/test/ruby_lsp_rails/document_symbol_test.rb b/test/ruby_lsp_rails/document_symbol_test.rb index 9896519f..4f787622 100644 --- a/test/ruby_lsp_rails/document_symbol_test.rb +++ b/test/ruby_lsp_rails/document_symbol_test.rb @@ -439,6 +439,36 @@ class FooModel < ApplicationRecord assert_empty(response[0].children) end + test "adds symbols for table names in schema.rb" do + response = generate_document_symbols_for_source(<<~RUBY) + ActiveRecord::Schema[8.1].define(version: 2025_10_07_010540) do + create_table "action_text_rich_texts", force: :cascade do |t| + t.text "body" + t.datetime "created_at", null: false + t.string "name", null: false + t.bigint "record_id", null: false + t.string "record_type", null: false + t.datetime "updated_at", null: false + t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true + end + + create_table "active_storage_attachments", force: :cascade do |t| + t.bigint "blob_id", null: false + t.datetime "created_at", null: false + t.string "name", null: false + t.bigint "record_id", null: false + t.string "record_type", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + end + RUBY + + assert_equal(2, response.size) + assert_equal("action_text_rich_texts", response[0].name) + assert_equal("active_storage_attachments", response[1].name) + end + private def generate_document_symbols_for_source(source) From 021a4060a116ca8fe7e6cd5a88d1127891f05bd2 Mon Sep 17 00:00:00 2001 From: Jose Camacho Date: Wed, 12 Nov 2025 14:33:11 -0600 Subject: [PATCH 2/5] don't push to the namespace stack --- .../ruby_lsp_rails/document_symbol.rb | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb b/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb index bff0c22f..d4463a02 100644 --- a/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb +++ b/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb @@ -24,12 +24,16 @@ def initialize(response_builder, dispatcher) :on_class_node_leave, :on_module_node_enter, :on_module_node_leave, - :on_constant_path_node_enter, ) end #: (Prism::CallNode node) -> void def on_call_node_enter(node) + message = node.message + return unless message + + handle_schema_table(node) + return if @namespace_stack.empty? content = extract_test_case_name(node) @@ -45,9 +49,6 @@ def on_call_node_enter(node) receiver = node.receiver return if receiver && !receiver.is_a?(Prism::SelfNode) - message = node.message - return unless message - case message when *Support::Callbacks::ALL, "validate" handle_all_arg_types(node, message) @@ -56,8 +57,6 @@ def on_call_node_enter(node) handle_symbol_and_string_arg_types(node, message) when "validates_with" handle_class_arg_types(node, message) - when "create_table" - handle_create_table(node) end end @@ -81,14 +80,6 @@ def on_module_node_leave(node) remove_from_namespace_stack(node) end - def on_constant_path_node_enter(node) - return if node.is_a?(Prism::SelfNode) || node.parent.is_a?(Prism::SelfNode) - - return unless node.parent.name == :ActiveRecord && node.name == :Schema - - @namespace_stack << node.full_name - end - private #: ((Prism::ClassNode | Prism::ModuleNode) node) -> void @@ -224,8 +215,10 @@ def handle_class_arg_types(node, message) end end - #: (Prism::CallNode node, String message) -> void - def handle_create_table(node) + #: (Prism::CallNode node) -> void + def handle_schema_table(node) + return unless node.message == "create_table" + table_name_argument = node.arguments.arguments.first append_document_symbol( From eff0235f135e2e10a40a7d9aeecb52bac0fc5770 Mon Sep 17 00:00:00 2001 From: Jose Camacho Date: Wed, 12 Nov 2025 16:41:50 -0600 Subject: [PATCH 3/5] add support for symbol argument, sorbet checks --- lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb | 11 ++++++++--- test/ruby_lsp_rails/document_symbol_test.rb | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb b/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb index d4463a02..ae172d36 100644 --- a/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb +++ b/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb @@ -219,12 +219,17 @@ def handle_class_arg_types(node, message) def handle_schema_table(node) return unless node.message == "create_table" - table_name_argument = node.arguments.arguments.first + table_name_argument = node.arguments&.arguments&.first + + return unless table_name_argument + + return unless table_name_argument.is_a?(Prism::StringNode) || + table_name_argument.is_a?(Prism::SymbolNode) append_document_symbol( - name: table_name_argument.content, + name: table_name_argument.unescaped, range: range_from_location(table_name_argument.location), - selection_range: range_from_location(table_name_argument.content_loc), + selection_range: range_from_location(table_name_argument.location), ) end diff --git a/test/ruby_lsp_rails/document_symbol_test.rb b/test/ruby_lsp_rails/document_symbol_test.rb index 4f787622..b9045962 100644 --- a/test/ruby_lsp_rails/document_symbol_test.rb +++ b/test/ruby_lsp_rails/document_symbol_test.rb @@ -452,7 +452,7 @@ class FooModel < ApplicationRecord t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true end - create_table "active_storage_attachments", force: :cascade do |t| + create_table :active_storage_attachments, force: :cascade do |t| t.bigint "blob_id", null: false t.datetime "created_at", null: false t.string "name", null: false From 6938d83f7f5079ad5767cc6d3bc31a6a60868f85 Mon Sep 17 00:00:00 2001 From: Jose Camacho Date: Wed, 12 Nov 2025 17:39:13 -0600 Subject: [PATCH 4/5] handle symbols and string in the same way --- .../ruby_lsp_rails/document_symbol.rb | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb b/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb index ae172d36..da56858b 100644 --- a/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb +++ b/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb @@ -223,14 +223,28 @@ def handle_schema_table(node) return unless table_name_argument - return unless table_name_argument.is_a?(Prism::StringNode) || - table_name_argument.is_a?(Prism::SymbolNode) + case table_name_argument + when Prism::SymbolNode + name = table_name_argument.value + return unless name - append_document_symbol( - name: table_name_argument.unescaped, - range: range_from_location(table_name_argument.location), - selection_range: range_from_location(table_name_argument.location), - ) + append_document_symbol( + name: name, + range: range_from_location(table_name_argument.location), + selection_range: range_from_location( + table_name_argument.value_loc, #: as !nil + ), + ) + when Prism::StringNode + name = table_name_argument.content + return if name.empty? + + append_document_symbol( + name: name, + range: range_from_location(table_name_argument.location), + selection_range: range_from_location(table_name_argument.content_loc), + ) + end end #: (name: String, range: RubyLsp::Interface::Range, selection_range: RubyLsp::Interface::Range) -> void From 7a57f7c2ae18642aac11a8df3298abb827966ee9 Mon Sep 17 00:00:00 2001 From: Jose Camacho Date: Sun, 16 Nov 2025 12:14:19 -0600 Subject: [PATCH 5/5] add additional test case, ensure we are inside define block --- .../ruby_lsp_rails/document_symbol.rb | 23 +++++++++++++++++++ test/ruby_lsp_rails/document_symbol_test.rb | 17 ++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb b/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb index da56858b..e21026f1 100644 --- a/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb +++ b/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb @@ -16,10 +16,12 @@ class DocumentSymbol def initialize(response_builder, dispatcher) @response_builder = response_builder @namespace_stack = [] #: Array[String] + @inside_schema = false #: bool dispatcher.register( self, :on_call_node_enter, + :on_call_node_leave, :on_class_node_enter, :on_class_node_leave, :on_module_node_enter, @@ -32,6 +34,8 @@ def on_call_node_enter(node) message = node.message return unless message + @inside_schema = true if node_is_schema_define?(node) + handle_schema_table(node) return if @namespace_stack.empty? @@ -60,6 +64,11 @@ def on_call_node_enter(node) end end + #: (Prism::CallNode node) -> void + def on_call_node_leave(node) + @inside_schema = false if node_is_schema_define?(node) + end + #: (Prism::ClassNode node) -> void def on_class_node_enter(node) add_to_namespace_stack(node) @@ -217,6 +226,7 @@ def handle_class_arg_types(node, message) #: (Prism::CallNode node) -> void def handle_schema_table(node) + return unless @inside_schema return unless node.message == "create_table" table_name_argument = node.arguments&.arguments&.first @@ -256,6 +266,19 @@ def append_document_symbol(name:, range:, selection_range:) selection_range: selection_range, ) end + + #: (Prism::CallNode node) -> bool + def node_is_schema_define?(node) + return false if node.message != "define" + + schema_node = node.receiver + return false unless schema_node.is_a?(Prism::CallNode) + + active_record_node = schema_node.receiver + return false unless active_record_node.is_a?(Prism::ConstantPathNode) + + constant_name(active_record_node) == "ActiveRecord::Schema" + end end end end diff --git a/test/ruby_lsp_rails/document_symbol_test.rb b/test/ruby_lsp_rails/document_symbol_test.rb index b9045962..15864ab2 100644 --- a/test/ruby_lsp_rails/document_symbol_test.rb +++ b/test/ruby_lsp_rails/document_symbol_test.rb @@ -469,6 +469,23 @@ class FooModel < ApplicationRecord assert_equal("active_storage_attachments", response[1].name) end + test "should not add create_table symbol unless inside migration block" do + response = generate_document_symbols_for_source(<<~RUBY) + create_table "action_text_rich_texts", force: :cascade do |t| + t.text "body" + t.datetime "created_at", null: false + t.string "name", null: false + t.bigint "record_id", null: false + t.string "record_type", null: false + t.datetime "updated_at", null: false + t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true + end + end + RUBY + + assert_equal(0, response.size) + end + private def generate_document_symbols_for_source(source)