Skip to content

Commit

Permalink
feature: Render mixed-in methods and constants with --embed-mixins (#…
Browse files Browse the repository at this point in the history
…842)

* Embed mixed-in methods and constants with `--embed-mixins`

When `--embed-mixins` option is set:

- methods from an `extend`ed module are documented as singleton methods
- attrs from an `extend`ed module are documented as class attributes
- methods from an `include`ed module are documented as instance methods
- attrs from an `include`ed module are documented as instance attributes
- constants from an `include`ed module are documented

Sections are created when needed, and Darkfish's template annotates
each of these mixed-in CodeObjects. We also respect the mixin methods'
visibility.

This feature is inspired by Yard's option of the same name.

* Add comment to document why we set object visibility

Co-authored-by: Stan Lo <[email protected]>

* Add the mixin_from attribute to CodeObject's initializer

* Add test coverage for private mixed-in attributes.

---------

Co-authored-by: Stan Lo <[email protected]>
  • Loading branch information
flavorjones and st0012 authored Oct 17, 2024
1 parent e2fe488 commit 481c2ce
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 2 deletions.
6 changes: 6 additions & 0 deletions lib/rdoc/code_object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ class RDoc::CodeObject

attr_accessor :viewer

##
# When mixed-in to a class, this points to the Context in which it was originally defined.

attr_accessor :mixin_from

##
# Creates a new CodeObject that will document itself and its children

Expand All @@ -111,6 +116,7 @@ def initialize
@full_name = nil
@store = nil
@track_visibility = true
@mixin_from = nil

initialize_visibility
end
Expand Down
40 changes: 40 additions & 0 deletions lib/rdoc/code_object/class_module.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ def comment= comment # :nodoc:
def complete min_visibility
update_aliases
remove_nodoc_children
embed_mixins
update_includes
remove_invisible min_visibility
end
Expand Down Expand Up @@ -798,4 +799,43 @@ def update_extends
extends.uniq!
end

def embed_mixins
return unless options.embed_mixins

includes.each do |include|
next if String === include.module
include.module.method_list.each do |code_object|
add_method(prepare_to_embed(code_object))
end
include.module.constants.each do |code_object|
add_constant(prepare_to_embed(code_object))
end
include.module.attributes.each do |code_object|
add_attribute(prepare_to_embed(code_object))
end
end

extends.each do |ext|
next if String === ext.module
ext.module.method_list.each do |code_object|
add_method(prepare_to_embed(code_object, true))
end
ext.module.attributes.each do |code_object|
add_attribute(prepare_to_embed(code_object, true))
end
end
end

private

def prepare_to_embed(code_object, singleton=false)
code_object = code_object.dup
code_object.mixin_from = code_object.parent
code_object.singleton = true if singleton
set_current_section(code_object.section.title, code_object.section.comment)
# add_method and add_attribute will reassign self's visibility back to the method/attribute
# so we need to sync self's visibility with the object's to properly retain that information
self.visibility = code_object.visibility
code_object
end
end
18 changes: 17 additions & 1 deletion lib/rdoc/generator/template/darkfish/class.rhtml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@
<%- constants.each do |const| -%>
<dt id="<%= const.name %>"><%= const.name %>
<%- if const.comment then -%>
<dd><%= const.description.strip %>
<dd>
<%- if const.mixin_from then -%>
<div class="mixin-from">
Included from <a href="<%= klass.aref_to(const.mixin_from.path)%>"><%= const.mixin_from.full_name %></a>
</div>
<%- end -%>
<%= const.description.strip %>
<%- else -%>
<dd class="missing-docs">(Not documented)
<%- end -%>
Expand All @@ -79,6 +85,11 @@
</div>

<div class="method-description">
<%- if attrib.mixin_from then -%>
<div class="mixin-from">
<%= attrib.singleton ? "Extended" : "Included" %> from <a href="<%= klass.aref_to(attrib.mixin_from.path)%>"><%= attrib.mixin_from.full_name %></a>
</div>
<%- end -%>
<%- if attrib.comment then -%>
<%= attrib.description.strip %>
<%- else -%>
Expand Down Expand Up @@ -145,6 +156,11 @@
<pre><%= method.markup_code %></pre>
</div>
<%- end -%>
<%- if method.mixin_from then -%>
<div class="mixin-from">
<%= method.singleton ? "Extended" : "Included" %> from <a href="<%= klass.aref_to(method.mixin_from.path)%>"><%= method.mixin_from.full_name %></a>
</div>
<%- end -%>
<%- if method.comment then -%>
<%= method.description.strip %>
<%- else -%>
Expand Down
7 changes: 7 additions & 0 deletions lib/rdoc/generator/template/darkfish/css/rdoc.css
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,13 @@ main .aliases {
font-style: italic;
cursor: default;
}

main .mixin-from {
font-size: 80%;
font-style: italic;
margin-bottom: 0.75em;
}

main .method-description ul {
margin-left: 1.5em;
}
Expand Down
19 changes: 18 additions & 1 deletion lib/rdoc/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -344,13 +344,19 @@ class RDoc::Options
# Indicates if files of test suites should be skipped
attr_accessor :skip_tests

##
# Embed mixin methods, attributes, and constants into class documentation. Set via
# +--[no-]embed-mixins+ (Default is +false+.)
attr_accessor :embed_mixins

def initialize loaded_options = nil # :nodoc:
init_ivars
override loaded_options if loaded_options
end

def init_ivars # :nodoc:
@dry_run = false
@embed_mixins = false
@exclude = %w[
~\z \.orig\z \.rej\z \.bak\z
\.gemspec\z
Expand Down Expand Up @@ -401,6 +407,7 @@ def init_with map # :nodoc:
@encoding = encoding ? Encoding.find(encoding) : encoding

@charset = map['charset']
@embed_mixins = map['embed_mixins']
@exclude = map['exclude']
@generator_name = map['generator_name']
@hyperlink_all = map['hyperlink_all']
Expand Down Expand Up @@ -432,6 +439,7 @@ def override map # :nodoc:
end

@charset = map['charset'] if map.has_key?('charset')
@embed_mixins = map['embed_mixins'] if map.has_key?('embed_mixins')
@exclude = map['exclude'] if map.has_key?('exclude')
@generator_name = map['generator_name'] if map.has_key?('generator_name')
@hyperlink_all = map['hyperlink_all'] if map.has_key?('hyperlink_all')
Expand Down Expand Up @@ -460,11 +468,12 @@ def override map # :nodoc:
def == other # :nodoc:
self.class === other and
@encoding == other.encoding and
@embed_mixins == other.embed_mixins and
@generator_name == other.generator_name and
@hyperlink_all == other.hyperlink_all and
@line_numbers == other.line_numbers and
@locale == other.locale and
@locale_dir == other.locale_dir and
@locale_dir == other.locale_dir and
@main_page == other.main_page and
@markup == other.markup and
@op_dir == other.op_dir and
Expand Down Expand Up @@ -842,6 +851,14 @@ def parse argv

opt.separator nil

opt.on("--[no-]embed-mixins",
"Embed mixin methods, attributes, and constants",
"into class documentation. (default false)") do |value|
@embed_mixins = value
end

opt.separator nil

markup_formats = RDoc::Text::MARKUP_FORMAT.keys.sort

opt.on("--markup=MARKUP", markup_formats,
Expand Down
136 changes: 136 additions & 0 deletions test/rdoc/test_rdoc_class_module.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1500,4 +1500,140 @@ def test_update_extends_with_colons
assert_equal [a, c], @c1.extends
end

class TestRDocClassModuleMixins < XrefTestCase
def setup
super

klass_tl = @store.add_file("klass.rb")
@klass = klass_tl.add_class(RDoc::NormalClass, "Klass")

incmod_tl = @store.add_file("incmod.rb")
@incmod = incmod_tl.add_module(RDoc::NormalModule, "Incmod")

incmod_const = @incmod.add_constant(RDoc::Constant.new("INCMOD_CONST_WITHOUT_A_SECTION", nil, ""))
incmod_const = @incmod.add_constant(RDoc::Constant.new("INCMOD_CONST", nil, ""))
incmod_const.section = @incmod.add_section("Incmod const section")

incmod_method = @incmod.add_method(RDoc::AnyMethod.new(nil, "incmod_method_without_a_section"))
incmod_method = @incmod.add_method(RDoc::AnyMethod.new(nil, "incmod_method"))
incmod_method.section = @incmod.add_section("Incmod method section")

incmod_attr = @incmod.add_attribute(RDoc::Attr.new(nil, "incmod_attr_without_a_section", "RW", ""))
incmod_attr = @incmod.add_attribute(RDoc::Attr.new(nil, "incmod_attr", "RW", ""))
incmod_attr.section = @incmod.add_section("Incmod attr section")

incmod_private_method = @incmod.add_method(RDoc::AnyMethod.new(nil, "incmod_private_method"))
incmod_private_method.visibility = :private

incmod_private_attr = @incmod.add_attribute(RDoc::Attr.new(nil, "incmod_private_attr", "RW", ""))
incmod_private_attr.visibility = :private

extmod_tl = @store.add_file("extmod.rb")
@extmod = extmod_tl.add_module(RDoc::NormalModule, "Extmod")

extmod_method = @extmod.add_method(RDoc::AnyMethod.new(nil, "extmod_method_without_a_section"))
extmod_method = @extmod.add_method(RDoc::AnyMethod.new(nil, "extmod_method"))
extmod_method.section = @extmod.add_section("Extmod method section")

extmod_attr = @extmod.add_attribute(RDoc::Attr.new(nil, "extmod_attr_without_a_section", "RW", "", true))
extmod_attr = @extmod.add_attribute(RDoc::Attr.new(nil, "extmod_attr", "RW", "", true))
extmod_attr.section = @extmod.add_section("Extmod attr section")

extmod_private_method = @extmod.add_method(RDoc::AnyMethod.new(nil, "extmod_private_method"))
extmod_private_method.visibility = :private

extmod_private_attr = @extmod.add_attribute(RDoc::Attr.new(nil, "extmod_private_attr", "RW", "", true))
extmod_private_attr.visibility = :private

@klass.add_include(RDoc::Include.new("Incmod", nil))
@klass.add_extend(RDoc::Include.new("Extmod", nil))

@klass.add_include(RDoc::Include.new("ExternalInclude", nil))
@klass.add_extend(RDoc::Include.new("ExternalExtend", nil))
end

def test_embed_mixin_when_false_does_not_embed_anything
assert_false(@klass.options.embed_mixins)
@klass.complete(:protected)

refute_includes(@klass.constants.map(&:name), "INCMOD_CONST")
refute_includes(@klass.method_list.map(&:name), "incmod_method")
refute_includes(@klass.method_list.map(&:name), "extmod_method")
refute_includes(@klass.attributes.map(&:name), "incmod_attr")
refute_includes(@klass.attributes.map(&:name), "extmod_attr")
end

def test_embed_mixin_when_true_embeds_methods_and_constants
@klass.options.embed_mixins = true
@klass.complete(:protected)

# assert on presence and identity of methods and constants
constant = @klass.constants.find { |c| c.name == "INCMOD_CONST" }
assert(constant, "constant from included mixin should be present")
assert_equal(@incmod, constant.mixin_from)

instance_method = @klass.method_list.find { |m| m.name == "incmod_method" }
assert(instance_method, "instance method from included mixin should be present")
refute(instance_method.singleton)
assert_equal(@incmod, instance_method.mixin_from)

instance_attr = @klass.attributes.find { |a| a.name == "incmod_attr" }
assert(instance_attr, "instance attr from included mixin should be present")
refute(instance_attr.singleton)
assert_equal(@incmod, instance_attr.mixin_from)

refute(@klass.method_list.find { |m| m.name == "incmod_private_method" })
refute(@klass.attributes.find { |m| m.name == "incmod_private_attr" })

class_method = @klass.method_list.find { |m| m.name == "extmod_method" }
assert(class_method, "class method from extended mixin should be present")
assert(class_method.singleton)
assert_equal(@extmod, class_method.mixin_from)

class_attr = @klass.attributes.find { |a| a.name == "extmod_attr" }
assert(class_attr, "class attr from extended mixin should be present")
assert(class_attr.singleton)
assert_equal(@extmod, class_attr.mixin_from)

refute(@klass.method_list.find { |m| m.name == "extmod_private_method" })
refute(@klass.attributes.find { |m| m.name == "extmod_private_attr" })

# assert that sections are also imported
constant_section = @klass.sections.find { |s| s.title == "Incmod const section" }
assert(constant_section, "constant from included mixin should have a section")
assert_equal(constant_section, constant.section)

instance_method_section = @klass.sections.find { |s| s.title == "Incmod method section" }
assert(instance_method_section, "instance method from included mixin should have a section")
assert_equal(instance_method_section, instance_method.section)

instance_attr_section = @klass.sections.find { |s| s.title == "Incmod attr section" }
assert(instance_attr_section, "instance attr from included mixin should have a section")
assert_equal(instance_attr_section, instance_attr.section)

class_method_section = @klass.sections.find { |s| s.title == "Extmod method section" }
assert(class_method_section, "class method from extended mixin should have a section")
assert_equal(class_method_section, class_method.section)

class_attr_section = @klass.sections.find { |s| s.title == "Extmod attr section" }
assert(class_attr_section, "class attr from extended mixin should have a section")
assert_equal(class_attr_section, class_attr.section)

# and check that code objects without a section still have no section
constant = @klass.constants.find { |c| c.name == "INCMOD_CONST_WITHOUT_A_SECTION" }
assert_nil(constant.section.title)

instance_method = @klass.method_list.find { |c| c.name == "incmod_method_without_a_section" }
assert_nil(instance_method.section.title)

instance_attr = @klass.attributes.find { |c| c.name == "incmod_attr_without_a_section" }
assert_nil(instance_attr.section.title)

class_method = @klass.method_list.find { |c| c.name == "extmod_method_without_a_section" }
assert_nil(class_method.section.title)

class_attr = @klass.attributes.find { |c| c.name == "extmod_attr_without_a_section" }
assert_nil(class_attr.section.title)
end
end
end
15 changes: 15 additions & 0 deletions test/rdoc/test_rdoc_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def test_to_yaml
expected = {
'charset' => 'UTF-8',
'encoding' => encoding,
'embed_mixins' => false,
'exclude' => %w[~\z \.orig\z \.rej\z \.bak\z \.gemspec\z],
'hyperlink_all' => false,
'line_numbers' => false,
Expand Down Expand Up @@ -589,6 +590,20 @@ def test_parse_root
assert_includes @options.rdoc_include, @options.root.to_s
end

def test_parse_embed_mixins
assert_false(@options.embed_mixins)

out, err = capture_output { @options.parse(["--embed-mixins"]) }
assert_empty(out)
assert_empty(err)
assert_true(@options.embed_mixins)

out, err = capture_output { @options.parse(["--no-embed-mixins"]) }
assert_empty(out)
assert_empty(err)
assert_false(@options.embed_mixins)
end

def test_parse_tab_width
@options.parse %w[--tab-width=1]
assert_equal 1, @options.tab_width
Expand Down

0 comments on commit 481c2ce

Please sign in to comment.