diff --git a/lib/rdoc/code_object/any_method.rb b/lib/rdoc/code_object/any_method.rb index 2eb1d22c41..f2c2ba8a77 100644 --- a/lib/rdoc/code_object/any_method.rb +++ b/lib/rdoc/code_object/any_method.rb @@ -111,6 +111,7 @@ def call_seq def call_seq=(call_seq) return if call_seq.nil? || call_seq.empty? + invalidate_name_indexes unless @name @call_seq = call_seq end diff --git a/lib/rdoc/code_object/class_module.rb b/lib/rdoc/code_object/class_module.rb index e1c3105850..c18d702cfc 100644 --- a/lib/rdoc/code_object/class_module.rb +++ b/lib/rdoc/code_object/class_module.rb @@ -70,7 +70,7 @@ def self.from_module(class_type, mod) klass.section = mod.section klass.attributes.concat mod.attributes - klass.method_list.concat mod.method_list + klass.replace_methods mod.method_list klass.aliases.concat mod.aliases klass.external_aliases.concat mod.external_aliases klass.constants.concat mod.constants @@ -541,8 +541,7 @@ def merge(class_module) if add add_method meth else - @method_list.delete meth - @methods_hash.delete meth.pretty_name + remove_method meth end end diff --git a/lib/rdoc/code_object/context.rb b/lib/rdoc/code_object/context.rb index 4f28d62600..da354e18ca 100644 --- a/lib/rdoc/code_object/context.rb +++ b/lib/rdoc/code_object/context.rb @@ -9,6 +9,8 @@ class Context < CodeObject include Comparable + require_relative 'context/method_list' + ## # Types of methods @@ -61,9 +63,12 @@ class Context < CodeObject attr_reader :extends ## - # Methods defined in this context - - attr_reader :method_list + # A frozen snapshot of the methods defined in this context. + # Use #add_method, #remove_method or #replace_methods to change the list. + #: () -> Array[AnyMethod] + def method_list + @method_list.to_a.freeze + end ## # Name of this class excluding namespace. See also full_name @@ -132,7 +137,7 @@ def initialize # Sets the defaults for methods and so-forth def initialize_methods_etc - @method_list = [] + @method_list = MethodList.new @attributes = [] @aliases = [] @requires = [] @@ -766,7 +771,7 @@ def find_attribute_named(name) # Finds a class method with +name+ in this context def find_class_method_named(name) - @method_list.find { |meth| meth.singleton && meth.name == name } + @method_list.find_named(name) { |meth| meth.singleton } end ## @@ -813,7 +818,7 @@ def find_external_alias_named(name) # Finds an instance method with +name+ in this context def find_instance_method_named(name) - @method_list.find { |meth| !meth.singleton && meth.name == name } + @method_list.find_named(name) { |meth| !meth.singleton } end ## @@ -833,13 +838,7 @@ def find_local_symbol(symbol) # Finds a method named +name+ with singleton value +singleton+. def find_method(name, singleton) - @method_list.find { |m| - if m.singleton - m.name == name && m.singleton == singleton - else - m.name == name && !m.singleton && !singleton - end - } + @method_list.find_named(name) { |m| m.singleton ? m.singleton == singleton : !singleton } end ## @@ -852,7 +851,7 @@ def find_method_named(name) when /\A::/ find_method name[2..-1], true else - @method_list.find { |meth| meth.name == name } + @method_list.find_named(name) end end @@ -1048,6 +1047,7 @@ def remove_from_documentation? def remove_invisible(min_visibility) return if [:private, :nodoc].include? min_visibility remove_invisible_in @method_list, min_visibility + @method_list.invalidate_name_index remove_invisible_in @attributes, min_visibility remove_invisible_in @constants, min_visibility end @@ -1067,6 +1067,24 @@ def remove_invisible_in(array, min_visibility) # :nodoc: end end + ## + # Removes +method+ from the documentation list and method registrations. + #: (AnyMethod) -> void + def remove_method(method) + @method_list.delete method + @method_list.invalidate_name_index + @methods_hash.delete method.pretty_name + end + + ## + # Replaces the documentation method list with +methods+. + # Registrations in #methods_hash are unchanged, including filtered methods. + #: (Array[AnyMethod]) -> void + def replace_methods(methods) + @method_list.replace methods + @method_list.invalidate_name_index + end + ## # Tries to resolve unmatched aliases when a method or attribute has just # been added. diff --git a/lib/rdoc/code_object/context/method_list.rb b/lib/rdoc/code_object/context/method_list.rb new file mode 100644 index 0000000000..25b7d888aa --- /dev/null +++ b/lib/rdoc/code_object/context/method_list.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true +module RDoc + class Context + ## + # Internal method storage with a lazy name index. Keeping the index on the + # list lets shallow-copied class/module aliases share its invalidation. + # Context exposes only frozen snapshots and invalidates destructive changes. + + class MethodList < Array # :nodoc: + # Finds the first method named +name+ accepted by the optional block. + # Keep candidate buckets private so callers cannot mutate the name index. + + #: (String?) ?{ (AnyMethod) -> bool } -> AnyMethod? + def find_named(name) + index_names + + # Names are retained by reference. Mutable strings can change without + # name=, so only use the index when every name is already frozen. + # Stop at the first match instead of allocating all matching candidates. + if @mutable_names + return find { |method| method.name == name && (!block_given? || yield(method)) } + end + + candidates = @name_index[name] + return unless candidates + block_given? ? candidates.find { |method| yield method } : candidates.first + end + + #: () -> void + def index_names + indexed = @name_index && @name_index_token[0] + if !indexed || @indexed_length < length + index = indexed ? @name_index : {} + token = indexed ? @name_index_token : [true] + mutable_names = indexed && @mutable_names + + # Parsing aliases interleaves lookups with additions. Index just the + # appended tail rather than repeatedly rebuilding the whole list. + (indexed ? @indexed_length : 0).upto(length - 1) do |i| + method = self[i] + method_name = method.name + if method_name.frozen? + (index[method_name] ||= []) << method + else + mutable_names = true + end + method.track_name_index token unless method.frozen? + end + + @name_index_token = token + @name_index = index + @indexed_length = length + @mutable_names = mutable_names + end + end + private :index_names + + #: () -> void + def invalidate_name_index + @name_index_token[0] = false if @name_index_token + @name_index = nil + end + + end + end +end diff --git a/lib/rdoc/code_object/method_attr.rb b/lib/rdoc/code_object/method_attr.rb index a760a89aae..53f1e6e356 100644 --- a/lib/rdoc/code_object/method_attr.rb +++ b/lib/rdoc/code_object/method_attr.rb @@ -9,8 +9,32 @@ class MethodAttr < CodeObject ## # Name of this method/attribute. + # The supplied string is retained by reference and may be mutated in place. - attr_accessor :name + attr_reader :name + + #: (String?) -> String? + def name=(name) + invalidate_name_indexes + @name = name + end + + # Track only validity tokens, not the indexes or lists themselves. A method + # can appear in more than one list after module promotion or bulk replacement, + # and must invalidate all of their name indexes when renamed. + + #: (Array[bool]) -> void + def track_name_index(token) # :nodoc: + tokens = (@name_index_tokens ||= []) + tokens.reject! { |existing| !existing[0] } + tokens << token unless tokens.any? { |existing| existing.equal? token } + end + + #: () -> void + def invalidate_name_indexes # :nodoc: + @name_index_tokens&.each { |token| token[0] = false } + @name_index_tokens = nil + end ## # public, protected, private @@ -95,6 +119,7 @@ def initialize(name, singleton: false) def initialize_copy(other) # :nodoc: @full_name = nil + @name_index_tokens = nil end def initialize_visibility # :nodoc: diff --git a/lib/rdoc/parser/c.rb b/lib/rdoc/parser/c.rb index c8bc98e6ae..91ae1ceee0 100644 --- a/lib/rdoc/parser/c.rb +++ b/lib/rdoc/parser/c.rb @@ -250,7 +250,7 @@ def do_aliases # method that reference the same function. def add_alias(var_name, class_obj, old_name, new_name, comment, singleton:) - al = Alias.new old_name, new_name, comment, singleton: singleton + al = Alias.new(-old_name, -new_name, comment, singleton: singleton) al.record_location @top_level class_obj.add_alias al @stats.add_alias al @@ -850,7 +850,7 @@ def handle_attr(var_name, attr_name, read, write) name = attr_name.gsub(/rb_intern(?:_const)?\("([^"]+)"\)/, '\1') - attr = Attr.new name, rw, comment + attr = Attr.new(-name, rw, comment) attr.record_location @top_level class_obj.add_attribute attr @@ -1008,7 +1008,7 @@ def handle_method(type, var_name, meth_name, function, param_count, type = 'method' # force public end - meth_obj = AnyMethod.new meth_name, singleton: singleton + meth_obj = AnyMethod.new(-meth_name, singleton: singleton) meth_obj.c_function = function p_count = Integer(param_count) rescue -1 diff --git a/lib/rdoc/parser/rbs.rb b/lib/rdoc/parser/rbs.rb index 7ceda649d0..9e84194cdb 100644 --- a/lib/rdoc/parser/rbs.rb +++ b/lib/rdoc/parser/rbs.rb @@ -106,7 +106,7 @@ def merge_attribute_methods(context, name, rw, singleton, comment, type_signatur end def rdoc_method_name(decl) - rbs_constructor_decl?(decl) ? 'new' : decl.name.to_s + rbs_constructor_decl?(decl) ? 'new' : decl.name.name end def rdoc_method_singleton?(decl) @@ -135,7 +135,7 @@ def parse_attr_decl(decl, context) comment = rdoc_comment_for decl type_signature_lines = [decl.type.to_s] - name = decl.name.to_s + name = decl.name.name singleton = decl.kind == :singleton if attribute = context.find_attribute(name, singleton) merge_documentation attribute, comment, type_signature_lines if @@ -231,8 +231,8 @@ def parse_member_decl(decl, context) def parse_method_alias_decl(decl, context) alias_def = Alias.new( - decl.old_name.to_s, - decl.new_name.to_s, + decl.old_name.name, + decl.new_name.name, rdoc_comment_for(decl), singleton: decl.kind == :singleton ) diff --git a/lib/rdoc/parser/ruby.rb b/lib/rdoc/parser/ruby.rb index ab0544e0b4..3e09d9f828 100644 --- a/lib/rdoc/parser/ruby.rb +++ b/lib/rdoc/parser/ruby.rb @@ -361,7 +361,7 @@ def parse_comment_tomdoc(container, comment, line_no, start_line) name, = signature.split %r%[ \(]%, 2 - meth = AnyMethod.new name + meth = AnyMethod.new(-name) record_location(meth) meth.line = start_line meth.call_seq = signature @@ -437,7 +437,7 @@ def handle_meta_method_comment(comment, directives, node) if attributes attributes.each do |attr| - a = Attr.new(attr, rw, comment, singleton: @singleton) + a = Attr.new(-attr, rw, comment, singleton: @singleton) a.store = @store a.line = line_no a.visibility = visibility @@ -624,7 +624,7 @@ def add_alias_method(old_name, new_name, line_no) handle_code_object_directives(@container, directives) if directives return if document_suppressed? - a = Alias.new(old_name, new_name, comment, singleton: @singleton) + a = Alias.new(-old_name, -new_name, comment, singleton: @singleton) handle_modifier_directive(a, line_no) a.store = @store a.line = line_no @@ -645,7 +645,7 @@ def add_attributes(names, rw, line_no) return unless @container.document_children names.each do |symbol| - a = Attr.new(symbol.to_s, rw, comment, singleton: @singleton) + a = Attr.new(-symbol.to_s, rw, comment, singleton: @singleton) a.store = @store a.line = line_no a.type_signature_lines = type_signature_lines @@ -753,6 +753,9 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility: end end + # Keep parser-owned names immutable and deduplicated without freezing + # strings supplied to the code-object API. + meth.name = -meth.name record_location(meth) container.add_method(meth) token_stream_loader = @colorizer_context.token_stream_loader(node_id) if node_id @@ -1164,7 +1167,7 @@ def visit_def_node(node) # `def (unknown expression).method_name` is not documentable return end - name = node.name.to_s + name = node.name.name params, block_params, calls_super = MethodSignatureVisitor.scan_signature(node) @scanner.add_method( name, diff --git a/lib/rdoc/store.rb b/lib/rdoc/store.rb index 3b934dbab0..2dcd84cfcd 100644 --- a/lib/rdoc/store.rb +++ b/lib/rdoc/store.rb @@ -219,7 +219,7 @@ def clear_file_contributions(relative_name, keep_position: false) top_level.classes_or_modules.each do |cm| # Remove methods and attributes contributed by this file - cm.method_list.reject! { |m| m.file == top_level } + cm.replace_methods cm.method_list.reject { |m| m.file == top_level } cm.attributes.reject! { |a| a.file == top_level } # Rebuild methods_hash from remaining methods and attributes @@ -706,7 +706,7 @@ def load_all load_method module_name, method.full_name end - mod.method_list.replace loaded_methods + mod.replace_methods loaded_methods loaded_attributes = mod.attributes.map do |attribute| load_method module_name, attribute.full_name diff --git a/test/rdoc/code_object/class_module_test.rb b/test/rdoc/code_object/class_module_test.rb index 43c49332c2..04eca866c7 100644 --- a/test/rdoc/code_object/class_module_test.rb +++ b/test/rdoc/code_object/class_module_test.rb @@ -159,6 +159,21 @@ def test_from_module_comment assert_equal({ tl => ['really a class'] }, klass.comment_location) end + def test_from_module_method_lookup + method = @m1.add_method RDoc::AnyMethod.new('indexed') + assert_same method, @m1.find_method_named('indexed') + + klass = RDoc::ClassModule.from_module RDoc::NormalClass, @m1 + assert_same method, klass.find_method_named('indexed') + + method.name = 'renamed' + + assert_nil @m1.find_method_named('indexed') + assert_nil klass.find_method_named('indexed') + assert_same method, @m1.find_method_named('renamed') + assert_same method, klass.find_method_named('renamed') + end + def test_marshal_dump @store.path = Dir.tmpdir tl = @store.add_file 'file.rb' @@ -1045,8 +1060,15 @@ def test_merge_methods meth = cm2.add_method RDoc::AnyMethod.new('m4') meth.record_location tl1 + old_method = cm1.find_method_named('m3') + assert_nil cm1.find_method_named('m2') + cm1.merge cm2 + assert_nil cm1.find_method_named('m1') + assert_same meth, cm1.find_method_named('m4') + assert_not_same old_method, cm1.find_method_named('m3') + expected = [ RDoc::AnyMethod.new('m2'), RDoc::AnyMethod.new('m3'), diff --git a/test/rdoc/code_object/context/method_list_test.rb b/test/rdoc/code_object/context/method_list_test.rb new file mode 100644 index 0000000000..4422746ca0 --- /dev/null +++ b/test/rdoc/code_object/context/method_list_test.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true +require_relative '../../helper' + +class RDocContextMethodListTest < RDoc::TestCase + + def setup + super + + @instance = RDoc::AnyMethod.new('m') + @singleton = RDoc::AnyMethod.new('m', singleton: true) + @other = RDoc::AnyMethod.new('other', singleton: true) + @list = RDoc::Context::MethodList.new([@other, @instance, @singleton]) + end + + def test_find_named + assert_same @instance, @list.find_named('m') + assert_nil @list.find_named('missing') + refute_respond_to @list, :index_names + end + + def test_find_named_with_predicate + seen = [] + result = @list.find_named('m') do |method| + seen << method + method.singleton + end + + assert_same @singleton, result + assert_equal [@instance, @singleton], seen + assert_nil @list.find_named('m') { false } + assert_nil @list.find_named('missing') { flunk 'predicate called for a missing name' } + end + + def test_find_named_with_mutable_names + assert_same @instance, @list.find_named('m') + name = +'old' + method = RDoc::AnyMethod.new(name) + @list << method + assert_same name, method.name + refute_predicate name, :frozen? + assert_same method, @list.find_named('old') + + name.replace('new') + assert_nil @list.find_named('old') + assert_same method, @list.find_named('new') + + method.name = +'assigned' + refute_predicate method.name, :frozen? + assert_same method, @list.find_named('assigned') + method.name.replace('renamed') + assert_nil @list.find_named('assigned') + assert_same method, @list.find_named('renamed') + end + + def test_find_named_with_mutable_names_stops_at_first_match + @instance.name = +'m' + assert_same @instance, @list.find_named('m') + + def @singleton.name + raise 'method list was scanned past the first match' + end + + assert_same @instance, @list.find_named('m') + assert_same @instance, @list.find_named('m') { true } + end + + def test_find_named_with_mutable_names_and_predicate + @instance.name = +'m' + seen = [] + + assert_same @singleton, @list.find_named('m') { |method| seen << method; method.singleton } + assert_equal [@instance, @singleton], seen + assert_nil @list.find_named('m') { false } + assert_nil @list.find_named('missing') { flunk 'predicate called for a missing name' } + end + +end diff --git a/test/rdoc/parser/c_test.rb b/test/rdoc/parser/c_test.rb index c2b92ae053..fdcccbfacd 100644 --- a/test/rdoc/parser/c_test.rb +++ b/test/rdoc/parser/c_test.rb @@ -273,6 +273,29 @@ def test_do_instance_duplicate_define_method assert_equal @top_level, methods.last.file end + def test_scan_freezes_method_and_attribute_names + content = <<~C + VALUE greet(VALUE self) { + } + + void Init_Sample(void) { + cSample = rb_define_class("Sample", rb_cObject); + rb_define_method(cSample, "greet", greet, 0); + rb_define_method(cSample, "salutation", greet, 0); + rb_define_singleton_method(cSample, "greet", greet, 0); + rb_define_alias(cSample, "hello", "greet"); + rb_define_attr(cSample, "name", 1, 0); + } + C + + klass = util_get_class content, 'cSample' + assert_equal %w[greet salutation greet hello], klass.method_list.map(&:name) + (klass.method_list + klass.attributes).each do |method| + assert_predicate method.name, :frozen?, method.full_name + end + assert_same klass.find_method('greet', false).name, klass.find_method('greet', true).name + end + def test_do_singleton_define_alias content = <<~C /* diff --git a/test/rdoc/parser/rbs_test.rb b/test/rdoc/parser/rbs_test.rb index 0365462cc1..0d27313c99 100644 --- a/test/rdoc/parser/rbs_test.rb +++ b/test/rdoc/parser/rbs_test.rb @@ -79,6 +79,27 @@ class Foo::Baz assert_same baz, foo.classes_hash['Baz'] end + def test_scan_freezes_method_and_attribute_names + util_parser(<<~RBS).scan + class Sample + def initialize: () -> void + def greet: () -> String + def self.greet: () -> String + alias salutation greet + alias later_alias later + def later: () -> void + attr_reader name: String + end + RBS + + sample = @store.find_class_named 'Sample' + assert_equal %w[new greet greet salutation later later_alias], sample.method_list.map(&:name) + (sample.method_list + sample.attributes).each do |method| + assert_predicate method.name, :frozen?, method.full_name + end + assert_same sample.find_method('greet', false).name, sample.find_method('greet', true).name + end + def test_scan_qualifies_nested_mixins util_parser(<<~RBS).scan module Sample diff --git a/test/rdoc/parser/ruby_test.rb b/test/rdoc/parser/ruby_test.rb index 4ac663abb4..7479394d8b 100644 --- a/test/rdoc/parser/ruby_test.rb +++ b/test/rdoc/parser/ruby_test.rb @@ -967,6 +967,110 @@ def two assert_equal 'my method two', two.comment.text.strip end + def test_method_names_are_frozen + util_parser <<~RUBY + module Foo + def one; end + def self.two; end + class << self + def three; end + end + alias four one + alias_method :five, :one + alias six later + def later; end + module_function :one + attr_accessor :attribute + end + RUBY + + mod = @store.find_module_named 'Foo' + assert_equal %w[one two three four five later six one], mod.method_list.map(&:name) + (mod.method_list + mod.attributes).each do |method| + assert_predicate method.name, :frozen?, method.full_name + end + assert_same mod.method_list.first.name, mod.method_list.last.name + + # Parser-owned immutable names must not change the public mutable-name API. + assert_same mod.method_list.first, mod.find_method_named('one') + name = +'external' + method = mod.add_method RDoc::AnyMethod.new(name) + assert_same name, method.name + assert_same method, mod.find_method_named('external') + name.replace('renamed') + assert_nil mod.find_method_named('external') + assert_same method, mod.find_method_named('renamed') + end + + def test_method_name_lookups_use_index + util_parser <<~RUBY + class Foo + def one; end + def self.one; end + end + RUBY + + klass = @store.find_class_named 'Foo' + instance, singleton = klass.method_list + assert_same instance, klass.find_method_named('one') + + klass.method_list.each do |method| + def method.name + raise 'parsed method names were scanned again' + end + end + + assert_same instance, klass.find_method('one', false) + assert_same singleton, klass.find_method('one', true) + assert_same instance, klass.find_method_named('#one') + assert_same singleton, klass.find_method_named('::one') + assert_same instance, klass.find_instance_method_named('one') + assert_same singleton, klass.find_class_method_named('one') + assert_nil klass.find_method_named('missing') + + added = klass.add_method RDoc::AnyMethod.new('added') + assert_same added, klass.find_method_named('added') + assert_same instance, klass.find_method_named('one') + end + + def test_meta_method_names_are_frozen + util_parser <<~RUBY + class Foo + ## + # Symbol argument. + define_method :one do end + + ## + # String argument. + define_method "two" do end + + ## + # :method: three + + ## + # :singleton-method: four + + ## + # :method: + # :call-seq: + # five(name) + + ## + # Unknown name. + define_method(name) do end + + ## + # :attr_reader: attribute + end + RUBY + + klass = @store.find_class_named 'Foo' + assert_equal %w[one two three four five unknown], klass.method_list.map(&:name) + (klass.method_list + klass.attributes).each do |method| + assert_predicate method.name, :frozen?, method.full_name + end + end + def test_method_toplevel util_parser <<~RUBY # comment @@ -2849,6 +2953,7 @@ class C m = c.method_list.first assert_equal "find_by_[_and_...]", m.name + assert_predicate m.name, :frozen? assert_equal "find_by_[_and_...](args)\n", m.call_seq expected = diff --git a/test/rdoc/rdoc_context_test.rb b/test/rdoc/rdoc_context_test.rb index d193b324cd..fb33c01956 100644 --- a/test/rdoc/rdoc_context_test.rb +++ b/test/rdoc/rdoc_context_test.rb @@ -29,6 +29,56 @@ def test_initialize assert_empty @context.constants end + def test_method_list_read_only_snapshot + method = @context.add_method RDoc::AnyMethod.new('m') + snapshot = @context.method_list + + assert_instance_of Array, snapshot + assert_predicate snapshot, :frozen? + assert_raise(FrozenError) { snapshot.clear } + + added = @context.add_method RDoc::AnyMethod.new('added') + + assert_equal [method], snapshot + assert_equal [method, added], @context.method_list + assert_same added, @context.find_method_named('added') + end + + def test_replace_methods + old = @context.add_method RDoc::AnyMethod.new('old') + assert_same old, @context.find_method_named('old') + assert_nil @context.find_method_named('new') + method = RDoc::AnyMethod.new 'new' + methods = [method] + + @context.replace_methods methods + methods.clear + + assert_equal [method], @context.method_list + assert_nil @context.find_method_named('old') + assert_same method, @context.find_method_named('new') + assert_same old, @context.methods_hash['#old'] + + method.name = 'renamed' + assert_nil @context.find_method_named('new') + assert_same method, @context.find_method_named('renamed') + end + + def test_remove_method + method = @context.add_method RDoc::AnyMethod.new('m') + retained = @context.add_method RDoc::AnyMethod.new('retained') + assert_same method, @context.find_method_named('m') + + @context.remove_method method + + assert_nil @context.find_method_named('m') + assert_nil @context.methods_hash['#m'] + assert_same retained, @context.find_method_named('retained') + + replacement = @context.add_method RDoc::AnyMethod.new('m') + assert_same replacement, @context.find_method_named('m') + end + def test_add_alias as = RDoc::Alias.new 'old_name', 'new_name', 'comment' @@ -588,6 +638,170 @@ def test_find_method_named assert_equal true, @c1.find_method_named('m').singleton end + def test_find_method_named_order + instance = @context.add_method RDoc::AnyMethod.new('m') + singleton = @context.add_method RDoc::AnyMethod.new('m', singleton: true) + + assert_same instance, @context.find_method_named('m') + assert_same instance, @context.find_method_named('#m') + assert_same singleton, @context.find_method_named('::m') + + @context.replace_methods @context.method_list.reverse + + assert_same singleton, @context.find_method_named('m') + assert_same instance, @context.find_method_named('#m') + assert_same singleton, @context.find_method_named('::m') + end + + def test_find_method_named_excludes_attributes + @context.add_attribute RDoc::Attr.new('attr', 'RW', '') + + %w[attr attr= #attr #attr= ::attr].each do |name| + assert_nil @context.find_method_named(name) + end + end + + def test_find_method_named_does_not_rescan + method = @context.add_method RDoc::AnyMethod.new('m') + assert_same method, @context.find_method_named('m') + + def method.name + raise 'method list was scanned again' + end + + assert_same method, @context.find_method_named('m') + assert_same method, @context.find_method_named('#m') + assert_same method, @context.find_instance_method_named('m') + assert_nil @context.find_method_named('missing') + assert_nil @context.find_method_named('::missing') + assert_nil @context.find_class_method_named('missing') + end + + def test_find_method_named_after_addition + assert_nil @context.find_method_named('m') + method = @context.add_method RDoc::AnyMethod.new('m') + assert_same method, @context.find_method_named('m') + end + + def test_find_method_named_appends_without_rescanning + method = @context.add_method RDoc::AnyMethod.new('m') + assert_same method, @context.find_method_named('m') + + def method.name + raise 'existing methods were rescanned after an append' + end + + added = @context.add_method RDoc::AnyMethod.new('added') + assert_same added, @context.find_method_named('added') + assert_same method, @context.find_method_named('m') + + assert_nil @context.find_method_named('aliased') + @context.add_alias RDoc::Alias.new('m', 'aliased', '') + + assert_same method, @context.find_method_named('aliased').is_alias_for + assert_same method, @context.find_method_named('m') + end + + def test_find_method_named_after_unmatched_alias + @context.add_alias RDoc::Alias.new('m', 'alias', '') + assert_nil @context.find_method_named('alias') + + method = @context.add_method RDoc::AnyMethod.new('m') + + assert_same method, @context.find_method_named('alias').is_alias_for + end + + def test_find_method_named_after_visibility_filter + util_visibilities + assert_same @priv, @vis.find_method_named('priv') + assert_same @prot, @vis.find_method_named('#prot') + + @vis.remove_invisible :public + + assert_nil @vis.find_method_named('priv') + assert_nil @vis.find_method_named('#prot') + assert_same @pub, @vis.find_method_named('pub') + assert_same @priv, @vis.methods_hash['#priv'] + end + + def test_find_method_named_after_rename + method = @context.add_method RDoc::AnyMethod.new('old') + assert_same method, @context.find_method_named('old') + assert_nil @context.find_method_named('new') + + @context.method_list.first.name = 'new' + + assert_nil @context.find_method_named('old') + assert_same method, @context.find_method_named('new') + assert_same method, @context.find_method_named('#new') + end + + def test_find_method_named_after_singleton_change + method = @context.add_method RDoc::AnyMethod.new('m') + assert_same method, @context.find_method_named('#m') + + method.singleton = true + + assert_nil @context.find_method_named('#m') + assert_same method, @context.find_method_named('::m') + assert_same method, @context.find_class_method_named('m') + assert_nil @context.find_instance_method_named('m') + end + + def test_find_method_nil_singleton + method = @context.add_method RDoc::AnyMethod.new('m', singleton: nil) + + assert_same method, @context.find_method('m', nil) + assert_same method, @context.find_method('m', false) + assert_same method, @context.find_method_named('#m') + assert_nil @context.find_method('m', true) + end + + def test_find_method_named_shared_list + method = @context.add_method RDoc::AnyMethod.new('m') + copy = @context.dup + assert_same method, @context.find_method_named('m') + assert_same method, copy.find_method_named('m') + + added = copy.add_method RDoc::AnyMethod.new('new') + assert_same added, @context.find_method_named('new') + + method.name = 'renamed' + assert_nil @context.find_method_named('m') + assert_nil copy.find_method_named('m') + assert_same method, copy.find_method_named('renamed') + + copy.replace_methods [] + assert_nil @context.find_method_named('renamed') + end + + def test_find_method_named_frozen_method + method = @context.add_method RDoc::AnyMethod.new('m') + method.freeze + + assert_same method, @context.find_method_named('m') + assert_nil @context.find_method_named('missing') + end + + def test_find_method_named_from_call_seq + method = RDoc::AnyMethod.new nil + @context.replace_methods [method] + assert_nil @context.find_method_named('m') + + method.call_seq = 'object.m()' + + assert_same method, @context.find_method_named('m') + end + + def test_find_method_named_after_reset + method = @context.add_method RDoc::AnyMethod.new('m') + assert_same method, @context.find_method_named('m') + + @context.initialize_methods_etc + + assert_nil @context.find_method_named('m') + end + def test_find_module_named assert_equal @c2_c3, @c2.find_module_named('C3') assert_equal @c2, @c2.find_module_named('C2') diff --git a/test/rdoc/rdoc_server_test.rb b/test/rdoc/rdoc_server_test.rb index 9f9a715713..03dac2ce91 100644 --- a/test/rdoc/rdoc_server_test.rb +++ b/test/rdoc/rdoc_server_test.rb @@ -120,6 +120,50 @@ def greet: () -> String end end + def test_check_for_changes_invalidates_method_lookup + other_file = File.join @dir, 'other.rb' + File.write other_file, "class Example\n def retained; end\nend\n" + capture_output { @rdoc.parse_files [other_file] } + @rdoc.store.complete @options.visibility + + example = @rdoc.store.find_class_or_module 'Example' + greet = example.find_method_named 'greet' + retained = example.find_method_named 'retained' + assert_not_nil greet + assert_not_nil retained + assert_nil example.find_method_named('farewell') + + with_running_server do |port| + assert_equal '200', get(port, '/Example.html').code + + File.write File.join(@dir, 'example.rb'), <<~RUBY + # Updated class + class Example + def farewell + end + end + RUBY + + wait_for('updated method to appear in the class page') do + get(port, '/Example.html').body.include?('method-i-farewell') + end + + assert_same example, @rdoc.store.find_class_or_module('Example') + assert_nil example.find_method_named('greet') + assert_not_nil example.find_method_named('farewell') + assert_same retained, example.find_method_named('retained') + + File.delete other_file + + wait_for('deleted file contributions to disappear') do + !get(port, '/Example.html').body.include?('method-i-retained') + end + + assert_nil example.find_method_named('retained') + assert_not_nil example.find_method_named('farewell') + end + end + def test_current_watch_files_deduplicates_symlinked_source_tree source_dir = File.join @dir, 'gem' symlink_dir = File.join @dir, 'docs', 'gem' diff --git a/test/rdoc/rdoc_store_test.rb b/test/rdoc/rdoc_store_test.rb index 6caccdc7b5..1ca57d74a9 100644 --- a/test/rdoc/rdoc_store_test.rb +++ b/test/rdoc/rdoc_store_test.rb @@ -392,8 +392,16 @@ def test_load_all s = RDoc::Store.new(RDoc::Options.new, path: @tmpdir) + loaded_class = s.load_class @klass.full_name + placeholder = loaded_class.find_method_named(@meth.name) + assert_not_nil placeholder + s.load_all + loaded_method = loaded_class.find_method_named @meth.name + assert_not_same placeholder, loaded_method + assert_equal @meth_comment.parse, loaded_method.comment.parse + assert_equal [@klass, @nest_klass], s.all_classes.sort assert_equal [@mod], s.all_modules.sort assert_equal [@page, @top_level], s.all_files.sort @@ -1159,8 +1167,14 @@ def test_clear_file_contributions_multi_file_class klass.add_comment 'comment from a', file_a klass.add_comment 'comment from b', file_b + assert_same meth_a, klass.find_method_named('from_a') + assert_same meth_b, klass.find_method_named('from_b') + @s.clear_file_contributions 'a.rb' + assert_nil klass.find_method_named('from_a') + assert_same meth_b, klass.find_method_named('from_b') + # Class is preserved because file_b still contributes assert_includes @s.classes_hash, 'MultiFileClass' @@ -1172,6 +1186,11 @@ def test_clear_file_contributions_multi_file_class # Comment from a.rb is removed, comment from b.rb remains assert_not_include klass.comment_location.keys, file_a assert_includes klass.comment_location.keys, file_b + + replacement = RDoc::AnyMethod.new 'from_a' + replacement.record_location file_a + klass.add_method replacement + assert_same replacement, klass.find_method_named('from_a') end def test_clear_file_contributions_cleans_methods_and_constants