diff --git a/lib/rdoc.rb b/lib/rdoc.rb index b62c22576d..7c81bd12fc 100644 --- a/lib/rdoc.rb +++ b/lib/rdoc.rb @@ -191,6 +191,7 @@ def self.home autoload :GhostMethod, "#{__dir__}/rdoc/ghost_method" autoload :MetaMethod, "#{__dir__}/rdoc/meta_method" autoload :Attr, "#{__dir__}/rdoc/attr" + autoload :Mattr, "#{__dir__}/rdoc/mattr" autoload :Constant, "#{__dir__}/rdoc/constant" autoload :Mixin, "#{__dir__}/rdoc/mixin" diff --git a/lib/rdoc/context.rb b/lib/rdoc/context.rb index c6edfb473c..3a0605eae9 100644 --- a/lib/rdoc/context.rb +++ b/lib/rdoc/context.rb @@ -29,6 +29,11 @@ class RDoc::Context < RDoc::CodeObject attr_reader :attributes + ## + # All mattr* methods + + attr_reader :mattrs + ## # Block params to be used in the next MethodAttr parsed under this context @@ -145,6 +150,7 @@ def initialize def initialize_methods_etc @method_list = [] @attributes = [] + @mattrs = [] @aliases = [] @requires = [] @includes = [] @@ -270,6 +276,63 @@ def add_attribute attribute attribute end + ## + # Adds +attribute+ if not already there. If it is (as method(s) or attribute), + # updates the comment if it was empty. + # + # The attribute is registered only if it defines a new method. + # For instance, attr_reader :foo will not be registered + # if method +foo+ exists, but attr_accessor :foo will be registered + # if method +foo+ exists, but foo= does not. + + def add_mattr attribute + return attribute unless @document_self + + # mainly to check for redefinition of an attribute as a method + # TODO find a policy for 'attr_reader :foo' + 'def foo=()' + register = false + + key = nil + + if attribute.rw.index 'R' then + key = attribute.pretty_name + known = @methods_hash[key] + + if known then + known.comment = attribute.comment if known.comment.empty? + elsif registered = @methods_hash[attribute.pretty_name + '='] and + RDoc::Mattr === registered then + registered.rw = 'RW' + else + @methods_hash[key] = attribute + register = true + end + end + + if attribute.rw.index 'W' then + key = attribute.pretty_name + '=' + known = @methods_hash[key] + + if known then + known.comment = attribute.comment if known.comment.empty? + elsif registered = @methods_hash[attribute.pretty_name] and + RDoc::Mattr === registered then + registered.rw = 'RW' + else + @methods_hash[key] = attribute + register = true + end + end + + if register then + attribute.visibility = @visibility + add_to @mattrs, attribute + resolve_aliases attribute + end + + attribute + end + ## # Adds a class named +given_name+ with +superclass+. # @@ -618,6 +681,7 @@ def any_content(includes = true) @comment.empty? && @method_list.empty? && @attributes.empty? && + @mattrs.empty? && @aliases.empty? && @external_aliases.empty? && @requires.empty? && @@ -720,6 +784,13 @@ def each_attribute # :yields: attribute @attributes.each { |a| yield a } end + ## + # Iterator for mattrs + + def each_mattr # :yields: attribute + @mattrs.each { |a| yield a } + end + ## # Iterator for classes and modules diff --git a/lib/rdoc/mattr.rb b/lib/rdoc/mattr.rb new file mode 100644 index 0000000000..6a352dbbea --- /dev/null +++ b/lib/rdoc/mattr.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true +## +# An attribute created by \#mattr_reader, \#mattr_writer or +# \#mattr_accessor + +class RDoc::Mattr < RDoc::MethodAttr + + ## + # 3:: + # RDoc 4 + # Added parent name and class + # Added section title + + MARSHAL_VERSION = 3 # :nodoc: + + ## + # Is the attribute readable ('R'), writable ('W') or both ('RW')? + + attr_accessor :rw + + ## + # Creates a new Mattr with body +text+, +name+, read/write status +rw+ and + # +comment+. +singleton+ marks this as a class attribute. + + def initialize(text, name, rw, comment, singleton = false) + super text, name + + @rw = rw + @singleton = singleton + self.comment = comment + end + + ## + # Mattrs are equal when their names, singleton and rw are identical + + def == other + self.class == other.class and + self.name == other.name and + self.rw == other.rw and + self.singleton == other.singleton + end + + ## + # Add +an_alias+ as an attribute in +context+. + + def add_alias(an_alias, context) + new_attr = self.class.new(self.text, an_alias.new_name, self.rw, + self.comment, self.singleton) + + new_attr.record_location an_alias.file + new_attr.visibility = self.visibility + new_attr.is_alias_for = self + @aliases << new_attr + context.add_attribute new_attr + new_attr + end + + ## + # The #aref prefix for mattrs + + def aref_prefix + 'mattr' + end + + ## + # Attributes never call super. See RDoc::AnyMethod#calls_super + # + # An RDoc::Mattr can show up in the method list in some situations (see + # Gem::ConfigFile) + + def calls_super # :nodoc: + false + end + + ## + # Returns mattr_reader, mattr_writer or mattr_accessor as appropriate. + + def definition + case @rw + when 'RW' then 'mattr_accessor' + when 'R' then 'mattr_reader' + when 'W' then 'mattr_writer' + end + end + + def inspect # :nodoc: + alias_for = @is_alias_for ? " (alias for #{@is_alias_for.name})" : nil + visibility = self.visibility + visibility = "forced #{visibility}" if force_documentation + "#<%s:0x%x %s %s (%s)%s>" % [ + self.class, object_id, + full_name, + rw, + visibility, + alias_for, + ] + end + + ## + # Dumps this Mattr for use by ri. See also #marshal_load + + def marshal_dump + [ MARSHAL_VERSION, + @name, + full_name, + @rw, + @visibility, + parse(@comment), + singleton, + @file.relative_name, + @parent.full_name, + @parent.class, + @section.title + ] + end + + ## + # Loads this Mattr from +array+. For a loaded Mattr the following + # methods will return cached values: + # + # * #full_name + # * #parent_name + + def marshal_load array + initialize_visibility + + @aliases = [] + @parent = nil + @parent_name = nil + @parent_class = nil + @section = nil + @file = nil + + version = array[0] + @name = array[1] + @full_name = array[2] + @rw = array[3] + @visibility = array[4] + @comment = array[5] + @singleton = array[6] || false # MARSHAL_VERSION == 0 + # 7 handled below + @parent_name = array[8] + @parent_class = array[9] + @section_title = array[10] + + @file = RDoc::TopLevel.new array[7] if version > 1 + + @parent_name ||= @full_name.split('#', 2).first + end + + def pretty_print q # :nodoc: + q.group 2, "[#{self.class.name} #{full_name} #{rw} #{visibility}", "]" do + unless comment.empty? then + q.breakable + q.text "comment:" + q.breakable + q.pp @comment + end + end + end + + def to_s # :nodoc: + "#{definition} #{name} in: #{parent}" + end + + ## + # Mattrs do not have token streams. + # + # An RDoc::Mattr can show up in the method list in some situations (see + # Gem::ConfigFile) + + def token_stream # :nodoc: + end + +end + diff --git a/lib/rdoc/parser/ruby.rb b/lib/rdoc/parser/ruby.rb index 0323e4de41..95667a6a52 100644 --- a/lib/rdoc/parser/ruby.rb +++ b/lib/rdoc/parser/ruby.rb @@ -290,6 +290,32 @@ def create_attr container, single, name, rw, comment # :nodoc: att end + ## + # Creates a new attribute in +container+ with +name+. + + def create_mattr container, single, name, rw, comment # :nodoc: + att = RDoc::Mattr.new get_tkread, name, rw, comment, single == SINGLE + record_location att + + container.add_mattr att + #@stats.add_mattr att + + att + end + + ## + # Creates a new attribute in +container+ with +name+. + + def create_thread_mattr container, single, name, rw, comment # :nodoc: + att = RDoc::ThreadMattr.new get_tkread, name, rw, comment, single == SINGLE + record_location att + + container.add_thread_mattr att + #@stats.add_thread_mattr att + + att + end + ## # Creates a module alias in +container+ at +rhs_name+ (or at the top-level # for "::") with the name from +constant+. @@ -753,6 +779,64 @@ def parse_attr_accessor(context, single, tk, comment) end end + ## + # Creates an RDoc::Mattr for each attribute listed after +tk+, setting the + # comment for each to +comment+. + + def parse_mattr_accessor(context, single, tk, comment) + line_no = tk[:line_no] + + args = parse_symbol_arg + rw = "?" + + tmp = RDoc::CodeObject.new + read_documentation_modifiers tmp, RDoc::ATTR_MODIFIERS + # TODO In most other places we let the context keep track of document_self + # and add found items appropriately but here we do not. I'm not sure why. + return if @track_visibility and not tmp.document_self + + case tk[:text] + when "mattr_reader" then rw = "R" + when "mattr_writer" then rw = "W" + when "mattr_accessor" then rw = "RW" + else + rw = '?' + end + + for name in args + att = create_mattr context, single, name, rw, comment + att.line = line_no + end + end + + ## + # Creates an RDoc::ThreadMattr for each attribute listed after +tk+, setting the + # comment for each to +comment+. + + def parse_thread_mattr_accessor(context, single, tk, comment) + line_no = tk[:line_no] + + args = parse_symbol_arg + rw = "?" + + tmp = RDoc::CodeObject.new + read_documentation_modifiers tmp, RDoc::ATTR_MODIFIERS + # TODO In most other places we let the context keep track of document_self + # and add found items appropriately but here we do not. I'm not sure why. + return if @track_visibility and not tmp.document_self + + case tk[:text] + when "thread_mattr_accessor" then rw = "RW" + else + rw = '?' + end + + for name in args + att = create_thread_mattr context, single, name, rw, comment + att.line = line_no + end + end + ## # Parses an +alias+ in +context+ with +comment+ @@ -1242,6 +1326,10 @@ def parse_identifier container, single, tk, comment # :nodoc: parse_attr container, single, tk, comment when /^attr_(reader|writer|accessor)$/ then parse_attr_accessor container, single, tk, comment + when /^mattr_(reader|writer|accessor)$/ then + parse_mattr_accessor container, single, tk, comment + when /^thread_mattr_(reader|writer|accessor)$/ then + parse_thread_mattr_accessor container, single, tk, comment when 'alias_method' then parse_alias container, single, tk, comment when 'require', 'include' then diff --git a/lib/rdoc/thread_mattr.rb b/lib/rdoc/thread_mattr.rb new file mode 100644 index 0000000000..c2af7bc489 --- /dev/null +++ b/lib/rdoc/thread_mattr.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true +## +# An thread mattributes created by \#thread_mattr_accessor + +class RDoc::ThreadMattr < RDoc::MethodAttr + + ## + # 3:: + # RDoc 4 + # Added parent name and class + # Added section title + + MARSHAL_VERSION = 3 # :nodoc: + + ## + # Is the attribute readable ('R'), writable ('W') or both ('RW')? + + attr_accessor :rw + + ## + # Creates a new ThreadMattr with body +text+, +name+, read/write status +rw+ and + # +comment+. +singleton+ marks this as a class attribute. + + def initialize(text, name, rw, comment, singleton = false) + super text, name + + @rw = rw + @singleton = singleton + self.comment = comment + end + + ## + # Thread Mattrs are equal when their names, singleton and rw are identical + + def == other + self.class == other.class and + self.name == other.name and + self.rw == other.rw and + self.singleton == other.singleton + end + + ## + # Add +an_alias+ as a threaded mattribute in +context+. + + def add_alias(an_alias, context) + new_attr = self.class.new(self.text, an_alias.new_name, self.rw, + self.comment, self.singleton) + + new_attr.record_location an_alias.file + new_attr.visibility = self.visibility + new_attr.is_alias_for = self + @aliases << new_attr + context.add_attribute new_attr + new_attr + end + + ## + # The #aref prefix for threaded mattributes + + def aref_prefix + 'thread_mattr' + end + + ## + # Attributes never call super. See RDoc::AnyMethod#calls_super + # + # An RDoc::Attr can show up in the method list in some situations (see + # Gem::ConfigFile) + + def calls_super # :nodoc: + false + end + + ## + # Returns attr_reader, attr_writer or attr_accessor as appropriate. + + def definition + case @rw + when 'RW' then 'thread_mattr_accessor' + when 'R' then 'thread_mattr_reader' + when 'W' then 'thread_mattr_writer' + end + end + + def inspect # :nodoc: + alias_for = @is_alias_for ? " (alias for #{@is_alias_for.name})" : nil + visibility = self.visibility + visibility = "forced #{visibility}" if force_documentation + "#<%s:0x%x %s %s (%s)%s>" % [ + self.class, object_id, + full_name, + rw, + visibility, + alias_for, + ] + end + + ## + # Dumps this ThreadMattr for use by ri. See also #marshal_load + + def marshal_dump + [ MARSHAL_VERSION, + @name, + full_name, + @rw, + @visibility, + parse(@comment), + singleton, + @file.relative_name, + @parent.full_name, + @parent.class, + @section.title + ] + end + + ## + # Loads this ThreadMattr from +array+. For a loaded ThreadMattr the following + # methods will return cached values: + # + # * #full_name + # * #parent_name + + def marshal_load array + initialize_visibility + + @aliases = [] + @parent = nil + @parent_name = nil + @parent_class = nil + @section = nil + @file = nil + + version = array[0] + @name = array[1] + @full_name = array[2] + @rw = array[3] + @visibility = array[4] + @comment = array[5] + @singleton = array[6] || false # MARSHAL_VERSION == 0 + # 7 handled below + @parent_name = array[8] + @parent_class = array[9] + @section_title = array[10] + + @file = RDoc::TopLevel.new array[7] if version > 1 + + @parent_name ||= @full_name.split('#', 2).first + end + + def pretty_print q # :nodoc: + q.group 2, "[#{self.class.name} #{full_name} #{rw} #{visibility}", "]" do + unless comment.empty? then + q.breakable + q.text "comment:" + q.breakable + q.pp @comment + end + end + end + + def to_s # :nodoc: + "#{definition} #{name} in: #{parent}" + end + + ## + # Thread mattributes do not have token streams. + # + # An RDoc::ThreadMattr can show up in the method list in some situations (see + # Gem::ConfigFile) + + def token_stream # :nodoc: + end + +end + diff --git a/test/rdoc/test_rdoc_mattr.rb b/test/rdoc/test_rdoc_mattr.rb new file mode 100644 index 0000000000..5a6274ee09 --- /dev/null +++ b/test/rdoc/test_rdoc_mattr.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true +require_relative 'helper' + +class TestRDocMattr < RDoc::TestCase + + def setup + super + + @a = RDoc::Mattr.new nil, 'mattr_accessor', 'RW', '' + end + + def test_aref + m = RDoc::Mattr.new nil, 'mattr_accessor', 'RW', nil + + assert_equal 'mattr-i-mattr_accessor', m.aref + end + + def test_arglists + assert_nil @a.arglists + end + + def test_block_params + assert_nil @a.block_params + end + + def test_call_seq + assert_nil @a.call_seq + end + + def test_definition + assert_equal 'mattr_accessor', @a.definition + + @a.rw = 'R' + + assert_equal 'mattr_reader', @a.definition + + @a.rw = 'W' + + assert_equal 'mattr_writer', @a.definition + end + + def test_full_name + assert_equal '(unknown)#mattr_accessor', @a.full_name + end + + def test_marshal_dump + tl = @store.add_file 'file.rb' + + @a.comment = 'this is a comment' + @a.record_location tl + + cm = tl.add_class RDoc::NormalClass, 'Klass' + cm.add_mattr @a + + section = cm.sections.first + + loaded = Marshal.load Marshal.dump @a + loaded.store = @store + + assert_equal @a, loaded + + comment = RDoc::Markup::Document.new( + RDoc::Markup::Paragraph.new('this is a comment')) + + assert_equal comment, loaded.comment + assert_equal 'file.rb', loaded.file.relative_name + assert_equal 'Klass#mattr_accessor', loaded.full_name + assert_equal 'mattr_accessor', loaded.name + assert_equal 'RW', loaded.rw + assert_equal false, loaded.singleton + assert_equal :public, loaded.visibility + assert_equal tl, loaded.file + assert_equal cm, loaded.parent + assert_equal section, loaded.section + end + + def test_marshal_dump_singleton + tl = @store.add_file 'file.rb' + + @a.comment = 'this is a comment' + @a.record_location tl + + cm = tl.add_class RDoc::NormalClass, 'Klass' + cm.add_mattr @a + + section = cm.sections.first + + @a.rw = 'R' + @a.singleton = true + @a.visibility = :protected + + loaded = Marshal.load Marshal.dump @a + loaded.store = @store + + assert_equal @a, loaded + + comment = RDoc::Markup::Document.new( + RDoc::Markup::Paragraph.new('this is a comment')) + + assert_equal comment, loaded.comment + assert_equal 'Klass::mattr_accessor', loaded.full_name + assert_equal 'mattr_accessor', loaded.name + assert_equal 'R', loaded.rw + assert_equal true, loaded.singleton + assert_equal :protected, loaded.visibility + assert_equal tl, loaded.file + assert_equal cm, loaded.parent + assert_equal section, loaded.section + end + + def test_params + assert_nil @a.params + end + + def test_singleton + refute @a.singleton + end + + def test_type + assert_equal 'instance', @a.type + + @a.singleton = true + assert_equal 'class', @a.type + end + +end