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