Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: support rails mattr_accessor variants #990

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/rdoc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
71 changes: 71 additions & 0 deletions lib/rdoc/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -145,6 +150,7 @@ def initialize
def initialize_methods_etc
@method_list = []
@attributes = []
@mattrs = []
@aliases = []
@requires = []
@includes = []
Expand Down Expand Up @@ -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, <tt>attr_reader :foo</tt> will not be registered
# if method +foo+ exists, but <tt>attr_accessor :foo</tt> will be registered
# if method +foo+ exists, but <tt>foo=</tt> 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+.
#
Expand Down Expand Up @@ -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? &&
Expand Down Expand Up @@ -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

Expand Down
176 changes: 176 additions & 0 deletions lib/rdoc/mattr.rb
Original file line number Diff line number Diff line change
@@ -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

88 changes: 88 additions & 0 deletions lib/rdoc/parser/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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+.
Expand Down Expand Up @@ -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+

Expand Down Expand Up @@ -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
Expand Down