From e258e17217ef2ab5b342626bdaac8068de8e7042 Mon Sep 17 00:00:00 2001 From: "Stanislav (Stas) Katkov" Date: Sat, 29 Aug 2026 16:59:47 +0200 Subject: [PATCH 1/7] RBS cache during scan --- lib/rdoc/parser/rbs.rb | 44 ++++++++++++++-- test/rdoc/parser/rbs_test.rb | 97 ++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/lib/rdoc/parser/rbs.rb b/lib/rdoc/parser/rbs.rb index 1733b90d56..b5ab07d131 100644 --- a/lib/rdoc/parser/rbs.rb +++ b/lib/rdoc/parser/rbs.rb @@ -11,6 +11,8 @@ class RDoc::Parser::RBS < RDoc::Parser parse_files_matching RBS_FILE_EXTENSION def scan + @attributes_by_context = {} + @methods_by_context = {} _, _, decls = ::RBS::Parser.parse_signature(@content) decls.each do |decl| parse_decl decl, @top_level @@ -90,12 +92,32 @@ def attr_rw_matches?(existing_rw, new_rw) existing_rw.each_char.any? { |rw| new_rw.include? rw } end + def attribute_index(context) + @attributes_by_context[context] ||= context.attributes.each_with_object({}) do |attribute, index| + index[[attribute.name, attribute.singleton]] ||= attribute + end + end + + def find_attribute(context, name, singleton) + attribute_index(context)[[name, singleton]] + end + + def method_index(context) + @methods_by_context[context] ||= context.method_list.each_with_object({}) do |method, index| + index[[method.name, !!method.singleton]] ||= method + end + end + + def find_method(context, name, singleton) + method_index(context)[[name, singleton]] + end + def merge_attribute_methods(context, name, rw, singleton, comment, type_signature_lines) method_names = [] method_names << name if rw.include?('R') method_names << "#{name}=" if rw.include?('W') - methods = method_names.map { |method_name| context.find_method(method_name, singleton) } + methods = method_names.map { |method_name| find_method(context, method_name, singleton) } methods.compact.each do |method| merge_documentation method, comment, type_signature_lines end @@ -135,7 +157,7 @@ def parse_attr_decl(decl, context) type_signature_lines = [decl.type.to_s] name = decl.name.to_s singleton = decl.kind == :singleton - if attribute = context.find_attribute(name, singleton) + if attribute = find_attribute(context, name, singleton) merge_documentation attribute, comment, type_signature_lines if attr_rw_matches? attribute.rw, rw return @@ -153,7 +175,11 @@ def parse_attr_decl(decl, context) ) record_object_location attribute, decl.location attribute.type_signature_lines = type_signature_lines + attribute_count = context.attributes.length context.add_attribute attribute + context.attributes[attribute_count..].each do |added_attribute| + attribute_index(context)[[added_attribute.name, added_attribute.singleton]] ||= added_attribute + end attribute.visibility = decl.visibility if decl.visibility end @@ -235,7 +261,15 @@ def parse_method_alias_decl(decl, context) singleton: decl.kind == :singleton ) record_object_location alias_def, decl.location + method_count = context.method_list.length + attribute_count = context.attributes.length context.add_alias alias_def + context.method_list[method_count..].each do |method| + method_index(context)[[method.name, !!method.singleton]] ||= method + end + context.attributes[attribute_count..].each do |attribute| + attribute_index(context)[[attribute.name, attribute.singleton]] ||= attribute + end end def parse_method_decl(decl, context) @@ -245,7 +279,7 @@ def parse_method_decl(decl, context) singleton = rdoc_method_singleton?(decl) visibility = rdoc_method_visibility(decl) - if method = context.find_method(method_name, singleton) + if method = find_method(context, method_name, singleton) merge_documentation method, comment, type_signature_lines return end @@ -260,7 +294,11 @@ def parse_method_decl(decl, context) end method.comment = comment if comment + method_count = context.method_list.length context.add_method method + context.method_list[method_count..].each do |added_method| + method_index(context)[[added_method.name, !!added_method.singleton]] ||= added_method + end method.visibility = visibility if visibility end diff --git a/test/rdoc/parser/rbs_test.rb b/test/rdoc/parser/rbs_test.rb index 0365462cc1..d90b3d9ff1 100644 --- a/test/rdoc/parser/rbs_test.rb +++ b/test/rdoc/parser/rbs_test.rb @@ -143,6 +143,71 @@ def greet: () -> String assert_equal ['() -> String'], greet.type_signature_lines end + def test_scan_finds_existing_method_after_store_index_rebuild + ruby_top_level = @store.add_file 'sample.rb' + sample = ruby_top_level.add_class RDoc::NormalClass, 'Sample' + + name = RDoc::AnyMethod.new 'name' + name.record_location ruby_top_level + sample.add_method name + + name_writer = RDoc::Attr.new 'name', 'W', nil + name_writer.record_location ruby_top_level + sample.add_attribute name_writer + + util_parser("class Sample\nend\n").scan + @store.clear_file_contributions @filename, keep_position: true + util_parser(<<~RBS).scan + class Sample + def name: () -> String + end + RBS + + assert_equal ['() -> String'], name.type_signature_lines + end + + def test_scan_indexes_forward_method_and_attribute_aliases + util_parser(<<~RBS).scan + class Sample + # Method alias docs. + alias salutation greet + alias display_name name + + def greet: () -> String + # Base attribute docs. + attr_reader name: String + + # Dedicated method docs. + def salutation: () -> String + # Dedicated attribute docs. + attr_reader display_name: String + end + RBS + + sample = @store.find_class_named 'Sample' + salutation = sample.find_method 'salutation', false + display_name = sample.find_attribute 'display_name', false + + assert_equal "Method alias docs.\n---\nDedicated method docs.", salutation.comment.to_s.strip + assert_equal "Base attribute docs.\n---\nDedicated attribute docs.", display_name.comment.to_s.strip + end + + def test_scan_treats_legacy_nil_singleton_as_instance_method + ruby_top_level = @store.add_file 'sample.rb' + sample = ruby_top_level.add_class RDoc::NormalClass, 'Sample' + greet = RDoc::AnyMethod.new 'greet' + greet.singleton = nil + sample.add_method greet + + util_parser(<<~RBS).scan + class Sample + def greet: () -> String + end + RBS + + assert_equal ['() -> String'], greet.type_signature_lines + end + def test_scan_preserves_rbs_markdown_when_extending_method_documentation ruby_top_level = @store.add_file 'sample.rb' sample = ruby_top_level.add_class RDoc::NormalClass, 'Sample' @@ -266,6 +331,38 @@ class PrivateSample assert_equal :public, private_constructor.visibility end + def test_scan_method_lookup_is_linear + name_calls = 0 + original_name = RDoc::AnyMethod.instance_method :name + RDoc::AnyMethod.define_method(:name) do + name_calls += 1 + original_name.bind_call self + end + + methods = 100.times.map { |i| " def m#{i}: () -> void" }.join("\n") + util_parser("class C\n#{methods}\nend\n").scan + + assert_operator name_calls, :<=, 1_000 + ensure + RDoc::AnyMethod.define_method :name, original_name + end + + def test_scan_attribute_lookup_is_linear + name_calls = 0 + original_name = RDoc::Attr.instance_method :name + RDoc::Attr.define_method(:name) do + name_calls += 1 + original_name.bind_call self + end + + attributes = 100.times.map { |i| " attr_reader a#{i}: String" }.join("\n") + util_parser("class C\n#{attributes}\nend\n").scan + + assert_operator name_calls, :<=, 1_000 + ensure + RDoc::Attr.remove_method :name + end + def util_parser(content) RDoc::Parser::RBS.new @top_level, content, @options, @stats end From 10264614e44c9804dbccbb32f50560df1e1a9bf9 Mon Sep 17 00:00:00 2001 From: "Stanislav (Stas) Katkov" Date: Sun, 30 Aug 2026 14:20:02 +0200 Subject: [PATCH 2/7] Lazily update RBS member indexes Context#add_alias can append methods or attributes as a side effect, making mutation-side cache updates depend on Context internals. Index each collection's unconsumed tail during lookup instead, keeping synchronization in one place while preserving linear lookup. --- lib/rdoc/parser/rbs.rb | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/lib/rdoc/parser/rbs.rb b/lib/rdoc/parser/rbs.rb index b5ab07d131..67bcff0e6a 100644 --- a/lib/rdoc/parser/rbs.rb +++ b/lib/rdoc/parser/rbs.rb @@ -93,9 +93,11 @@ def attr_rw_matches?(existing_rw, new_rw) end def attribute_index(context) - @attributes_by_context[context] ||= context.attributes.each_with_object({}) do |attribute, index| + index = @attributes_by_context[context] ||= {} + context.attributes[index.length..].each do |attribute| index[[attribute.name, attribute.singleton]] ||= attribute end + index end def find_attribute(context, name, singleton) @@ -103,9 +105,11 @@ def find_attribute(context, name, singleton) end def method_index(context) - @methods_by_context[context] ||= context.method_list.each_with_object({}) do |method, index| + index = @methods_by_context[context] ||= {} + context.method_list[index.length..].each do |method| index[[method.name, !!method.singleton]] ||= method end + index end def find_method(context, name, singleton) @@ -175,11 +179,7 @@ def parse_attr_decl(decl, context) ) record_object_location attribute, decl.location attribute.type_signature_lines = type_signature_lines - attribute_count = context.attributes.length context.add_attribute attribute - context.attributes[attribute_count..].each do |added_attribute| - attribute_index(context)[[added_attribute.name, added_attribute.singleton]] ||= added_attribute - end attribute.visibility = decl.visibility if decl.visibility end @@ -261,15 +261,7 @@ def parse_method_alias_decl(decl, context) singleton: decl.kind == :singleton ) record_object_location alias_def, decl.location - method_count = context.method_list.length - attribute_count = context.attributes.length context.add_alias alias_def - context.method_list[method_count..].each do |method| - method_index(context)[[method.name, !!method.singleton]] ||= method - end - context.attributes[attribute_count..].each do |attribute| - attribute_index(context)[[attribute.name, attribute.singleton]] ||= attribute - end end def parse_method_decl(decl, context) @@ -294,11 +286,7 @@ def parse_method_decl(decl, context) end method.comment = comment if comment - method_count = context.method_list.length context.add_method method - context.method_list[method_count..].each do |added_method| - method_index(context)[[added_method.name, !!added_method.singleton]] ||= added_method - end method.visibility = visibility if visibility end From d89bb9ec9eb31479c6c14b05e5f5ffc12733c9a7 Mon Sep 17 00:00:00 2001 From: "Stanislav (Stas) Katkov" Date: Sun, 30 Aug 2026 14:43:46 +0200 Subject: [PATCH 3/7] remove method mocking tests --- test/rdoc/parser/rbs_test.rb | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/test/rdoc/parser/rbs_test.rb b/test/rdoc/parser/rbs_test.rb index d90b3d9ff1..5199b61831 100644 --- a/test/rdoc/parser/rbs_test.rb +++ b/test/rdoc/parser/rbs_test.rb @@ -331,38 +331,6 @@ class PrivateSample assert_equal :public, private_constructor.visibility end - def test_scan_method_lookup_is_linear - name_calls = 0 - original_name = RDoc::AnyMethod.instance_method :name - RDoc::AnyMethod.define_method(:name) do - name_calls += 1 - original_name.bind_call self - end - - methods = 100.times.map { |i| " def m#{i}: () -> void" }.join("\n") - util_parser("class C\n#{methods}\nend\n").scan - - assert_operator name_calls, :<=, 1_000 - ensure - RDoc::AnyMethod.define_method :name, original_name - end - - def test_scan_attribute_lookup_is_linear - name_calls = 0 - original_name = RDoc::Attr.instance_method :name - RDoc::Attr.define_method(:name) do - name_calls += 1 - original_name.bind_call self - end - - attributes = 100.times.map { |i| " attr_reader a#{i}: String" }.join("\n") - util_parser("class C\n#{attributes}\nend\n").scan - - assert_operator name_calls, :<=, 1_000 - ensure - RDoc::Attr.remove_method :name - end - def util_parser(content) RDoc::Parser::RBS.new @top_level, content, @options, @stats end From aa935ee6e419d66ae940385c7493a0687ce2e786 Mon Sep 17 00:00:00 2001 From: "Stanislav (Stas) Katkov" Date: Sun, 30 Aug 2026 14:54:06 +0200 Subject: [PATCH 4/7] Revived both RBS linear-performance tests using assert_linear_performance --- test/rdoc/parser/rbs_test.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/rdoc/parser/rbs_test.rb b/test/rdoc/parser/rbs_test.rb index 5199b61831..a94153ef70 100644 --- a/test/rdoc/parser/rbs_test.rb +++ b/test/rdoc/parser/rbs_test.rb @@ -331,6 +331,20 @@ class PrivateSample assert_equal :public, private_constructor.visibility end + def test_scan_method_lookup_linear_performance + assert_linear_performance([1, 10, 100]) do |factor| + methods = (factor * 200).times.map { |i| " def m#{i}: () -> void" }.join("\n") + util_parser("class C\n#{methods}\nend\n").scan + end + end + + def test_scan_attribute_lookup_linear_performance + assert_linear_performance([1, 10, 100]) do |factor| + attributes = (factor * 200).times.map { |i| " attr_reader a#{i}: String" }.join("\n") + util_parser("class C\n#{attributes}\nend\n").scan + end + end + def util_parser(content) RDoc::Parser::RBS.new @top_level, content, @options, @stats end From 0ce0deebe94efaf273d7391da54859072677db95 Mon Sep 17 00:00:00 2001 From: "Stanislav (Stas) Katkov" Date: Sun, 30 Aug 2026 16:17:01 +0200 Subject: [PATCH 5/7] remove finder methods --- lib/rdoc/parser/rbs.rb | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/lib/rdoc/parser/rbs.rb b/lib/rdoc/parser/rbs.rb index 67bcff0e6a..77b35be478 100644 --- a/lib/rdoc/parser/rbs.rb +++ b/lib/rdoc/parser/rbs.rb @@ -100,10 +100,6 @@ def attribute_index(context) index end - def find_attribute(context, name, singleton) - attribute_index(context)[[name, singleton]] - end - def method_index(context) index = @methods_by_context[context] ||= {} context.method_list[index.length..].each do |method| @@ -112,16 +108,12 @@ def method_index(context) index end - def find_method(context, name, singleton) - method_index(context)[[name, singleton]] - end - def merge_attribute_methods(context, name, rw, singleton, comment, type_signature_lines) method_names = [] method_names << name if rw.include?('R') method_names << "#{name}=" if rw.include?('W') - methods = method_names.map { |method_name| find_method(context, method_name, singleton) } + methods = method_names.map { |method_name| method_index(context)[[method_name, singleton]] } methods.compact.each do |method| merge_documentation method, comment, type_signature_lines end @@ -161,7 +153,7 @@ def parse_attr_decl(decl, context) type_signature_lines = [decl.type.to_s] name = decl.name.to_s singleton = decl.kind == :singleton - if attribute = find_attribute(context, name, singleton) + if attribute = attribute_index(context)[[name, singleton]] merge_documentation attribute, comment, type_signature_lines if attr_rw_matches? attribute.rw, rw return @@ -271,7 +263,7 @@ def parse_method_decl(decl, context) singleton = rdoc_method_singleton?(decl) visibility = rdoc_method_visibility(decl) - if method = find_method(context, method_name, singleton) + if method = method_index(context)[[method_name, singleton]] merge_documentation method, comment, type_signature_lines return end From 9bbab84ae36d2f3e3ad525be9aa7692efb3c0cfa Mon Sep 17 00:00:00 2001 From: "Stanislav (Stas) Katkov" Date: Sun, 30 Aug 2026 16:17:15 +0200 Subject: [PATCH 6/7] simplify singleton setup --- test/rdoc/parser/rbs_test.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/rdoc/parser/rbs_test.rb b/test/rdoc/parser/rbs_test.rb index a94153ef70..d8ddf29b9c 100644 --- a/test/rdoc/parser/rbs_test.rb +++ b/test/rdoc/parser/rbs_test.rb @@ -195,8 +195,7 @@ def salutation: () -> String def test_scan_treats_legacy_nil_singleton_as_instance_method ruby_top_level = @store.add_file 'sample.rb' sample = ruby_top_level.add_class RDoc::NormalClass, 'Sample' - greet = RDoc::AnyMethod.new 'greet' - greet.singleton = nil + greet = RDoc::AnyMethod.new 'greet', singleton: nil sample.add_method greet util_parser(<<~RBS).scan From 5ab0b8b6f554c0414f0a84cb519652d0d6920ee5 Mon Sep 17 00:00:00 2001 From: "Stanislav (Stas) Katkov" Date: Sun, 30 Aug 2026 16:17:25 +0200 Subject: [PATCH 7/7] combine two tests into one --- test/rdoc/parser/rbs_test.rb | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/test/rdoc/parser/rbs_test.rb b/test/rdoc/parser/rbs_test.rb index d8ddf29b9c..9f0a95fcd0 100644 --- a/test/rdoc/parser/rbs_test.rb +++ b/test/rdoc/parser/rbs_test.rb @@ -330,17 +330,10 @@ class PrivateSample assert_equal :public, private_constructor.visibility end - def test_scan_method_lookup_linear_performance + def test_scan_member_lookup_linear_performance assert_linear_performance([1, 10, 100]) do |factor| - methods = (factor * 200).times.map { |i| " def m#{i}: () -> void" }.join("\n") - util_parser("class C\n#{methods}\nend\n").scan - end - end - - def test_scan_attribute_lookup_linear_performance - assert_linear_performance([1, 10, 100]) do |factor| - attributes = (factor * 200).times.map { |i| " attr_reader a#{i}: String" }.join("\n") - util_parser("class C\n#{attributes}\nend\n").scan + members = Array.new(factor * 200) { |i| " def m#{i}: () -> void\n attr_reader a#{i}: String" }.join("\n") + util_parser("class C\n#{members}\nend\n").scan end end