Skip to content

Commit 481c2ce

Browse files
flavorjonesst0012
andauthored
feature: Render mixed-in methods and constants with --embed-mixins (#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]>
1 parent e2fe488 commit 481c2ce

File tree

7 files changed

+239
-2
lines changed

7 files changed

+239
-2
lines changed

lib/rdoc/code_object.rb

+6
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ class RDoc::CodeObject
9696

9797
attr_accessor :viewer
9898

99+
##
100+
# When mixed-in to a class, this points to the Context in which it was originally defined.
101+
102+
attr_accessor :mixin_from
103+
99104
##
100105
# Creates a new CodeObject that will document itself and its children
101106

@@ -111,6 +116,7 @@ def initialize
111116
@full_name = nil
112117
@store = nil
113118
@track_visibility = true
119+
@mixin_from = nil
114120

115121
initialize_visibility
116122
end

lib/rdoc/code_object/class_module.rb

+40
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ def comment= comment # :nodoc:
223223
def complete min_visibility
224224
update_aliases
225225
remove_nodoc_children
226+
embed_mixins
226227
update_includes
227228
remove_invisible min_visibility
228229
end
@@ -798,4 +799,43 @@ def update_extends
798799
extends.uniq!
799800
end
800801

802+
def embed_mixins
803+
return unless options.embed_mixins
804+
805+
includes.each do |include|
806+
next if String === include.module
807+
include.module.method_list.each do |code_object|
808+
add_method(prepare_to_embed(code_object))
809+
end
810+
include.module.constants.each do |code_object|
811+
add_constant(prepare_to_embed(code_object))
812+
end
813+
include.module.attributes.each do |code_object|
814+
add_attribute(prepare_to_embed(code_object))
815+
end
816+
end
817+
818+
extends.each do |ext|
819+
next if String === ext.module
820+
ext.module.method_list.each do |code_object|
821+
add_method(prepare_to_embed(code_object, true))
822+
end
823+
ext.module.attributes.each do |code_object|
824+
add_attribute(prepare_to_embed(code_object, true))
825+
end
826+
end
827+
end
828+
829+
private
830+
831+
def prepare_to_embed(code_object, singleton=false)
832+
code_object = code_object.dup
833+
code_object.mixin_from = code_object.parent
834+
code_object.singleton = true if singleton
835+
set_current_section(code_object.section.title, code_object.section.comment)
836+
# add_method and add_attribute will reassign self's visibility back to the method/attribute
837+
# so we need to sync self's visibility with the object's to properly retain that information
838+
self.visibility = code_object.visibility
839+
code_object
840+
end
801841
end

lib/rdoc/generator/template/darkfish/class.rhtml

+17-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@
5454
<%- constants.each do |const| -%>
5555
<dt id="<%= const.name %>"><%= const.name %>
5656
<%- if const.comment then -%>
57-
<dd><%= const.description.strip %>
57+
<dd>
58+
<%- if const.mixin_from then -%>
59+
<div class="mixin-from">
60+
Included from <a href="<%= klass.aref_to(const.mixin_from.path)%>"><%= const.mixin_from.full_name %></a>
61+
</div>
62+
<%- end -%>
63+
<%= const.description.strip %>
5864
<%- else -%>
5965
<dd class="missing-docs">(Not documented)
6066
<%- end -%>
@@ -79,6 +85,11 @@
7985
</div>
8086

8187
<div class="method-description">
88+
<%- if attrib.mixin_from then -%>
89+
<div class="mixin-from">
90+
<%= attrib.singleton ? "Extended" : "Included" %> from <a href="<%= klass.aref_to(attrib.mixin_from.path)%>"><%= attrib.mixin_from.full_name %></a>
91+
</div>
92+
<%- end -%>
8293
<%- if attrib.comment then -%>
8394
<%= attrib.description.strip %>
8495
<%- else -%>
@@ -145,6 +156,11 @@
145156
<pre><%= method.markup_code %></pre>
146157
</div>
147158
<%- end -%>
159+
<%- if method.mixin_from then -%>
160+
<div class="mixin-from">
161+
<%= method.singleton ? "Extended" : "Included" %> from <a href="<%= klass.aref_to(method.mixin_from.path)%>"><%= method.mixin_from.full_name %></a>
162+
</div>
163+
<%- end -%>
148164
<%- if method.comment then -%>
149165
<%= method.description.strip %>
150166
<%- else -%>

lib/rdoc/generator/template/darkfish/css/rdoc.css

+7
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,13 @@ main .aliases {
670670
font-style: italic;
671671
cursor: default;
672672
}
673+
674+
main .mixin-from {
675+
font-size: 80%;
676+
font-style: italic;
677+
margin-bottom: 0.75em;
678+
}
679+
673680
main .method-description ul {
674681
margin-left: 1.5em;
675682
}

lib/rdoc/options.rb

+18-1
Original file line numberDiff line numberDiff line change
@@ -344,13 +344,19 @@ class RDoc::Options
344344
# Indicates if files of test suites should be skipped
345345
attr_accessor :skip_tests
346346

347+
##
348+
# Embed mixin methods, attributes, and constants into class documentation. Set via
349+
# +--[no-]embed-mixins+ (Default is +false+.)
350+
attr_accessor :embed_mixins
351+
347352
def initialize loaded_options = nil # :nodoc:
348353
init_ivars
349354
override loaded_options if loaded_options
350355
end
351356

352357
def init_ivars # :nodoc:
353358
@dry_run = false
359+
@embed_mixins = false
354360
@exclude = %w[
355361
~\z \.orig\z \.rej\z \.bak\z
356362
\.gemspec\z
@@ -401,6 +407,7 @@ def init_with map # :nodoc:
401407
@encoding = encoding ? Encoding.find(encoding) : encoding
402408

403409
@charset = map['charset']
410+
@embed_mixins = map['embed_mixins']
404411
@exclude = map['exclude']
405412
@generator_name = map['generator_name']
406413
@hyperlink_all = map['hyperlink_all']
@@ -432,6 +439,7 @@ def override map # :nodoc:
432439
end
433440

434441
@charset = map['charset'] if map.has_key?('charset')
442+
@embed_mixins = map['embed_mixins'] if map.has_key?('embed_mixins')
435443
@exclude = map['exclude'] if map.has_key?('exclude')
436444
@generator_name = map['generator_name'] if map.has_key?('generator_name')
437445
@hyperlink_all = map['hyperlink_all'] if map.has_key?('hyperlink_all')
@@ -460,11 +468,12 @@ def override map # :nodoc:
460468
def == other # :nodoc:
461469
self.class === other and
462470
@encoding == other.encoding and
471+
@embed_mixins == other.embed_mixins and
463472
@generator_name == other.generator_name and
464473
@hyperlink_all == other.hyperlink_all and
465474
@line_numbers == other.line_numbers and
466475
@locale == other.locale and
467-
@locale_dir == other.locale_dir and
476+
@locale_dir == other.locale_dir and
468477
@main_page == other.main_page and
469478
@markup == other.markup and
470479
@op_dir == other.op_dir and
@@ -842,6 +851,14 @@ def parse argv
842851

843852
opt.separator nil
844853

854+
opt.on("--[no-]embed-mixins",
855+
"Embed mixin methods, attributes, and constants",
856+
"into class documentation. (default false)") do |value|
857+
@embed_mixins = value
858+
end
859+
860+
opt.separator nil
861+
845862
markup_formats = RDoc::Text::MARKUP_FORMAT.keys.sort
846863

847864
opt.on("--markup=MARKUP", markup_formats,

test/rdoc/test_rdoc_class_module.rb

+136
Original file line numberDiff line numberDiff line change
@@ -1500,4 +1500,140 @@ def test_update_extends_with_colons
15001500
assert_equal [a, c], @c1.extends
15011501
end
15021502

1503+
class TestRDocClassModuleMixins < XrefTestCase
1504+
def setup
1505+
super
1506+
1507+
klass_tl = @store.add_file("klass.rb")
1508+
@klass = klass_tl.add_class(RDoc::NormalClass, "Klass")
1509+
1510+
incmod_tl = @store.add_file("incmod.rb")
1511+
@incmod = incmod_tl.add_module(RDoc::NormalModule, "Incmod")
1512+
1513+
incmod_const = @incmod.add_constant(RDoc::Constant.new("INCMOD_CONST_WITHOUT_A_SECTION", nil, ""))
1514+
incmod_const = @incmod.add_constant(RDoc::Constant.new("INCMOD_CONST", nil, ""))
1515+
incmod_const.section = @incmod.add_section("Incmod const section")
1516+
1517+
incmod_method = @incmod.add_method(RDoc::AnyMethod.new(nil, "incmod_method_without_a_section"))
1518+
incmod_method = @incmod.add_method(RDoc::AnyMethod.new(nil, "incmod_method"))
1519+
incmod_method.section = @incmod.add_section("Incmod method section")
1520+
1521+
incmod_attr = @incmod.add_attribute(RDoc::Attr.new(nil, "incmod_attr_without_a_section", "RW", ""))
1522+
incmod_attr = @incmod.add_attribute(RDoc::Attr.new(nil, "incmod_attr", "RW", ""))
1523+
incmod_attr.section = @incmod.add_section("Incmod attr section")
1524+
1525+
incmod_private_method = @incmod.add_method(RDoc::AnyMethod.new(nil, "incmod_private_method"))
1526+
incmod_private_method.visibility = :private
1527+
1528+
incmod_private_attr = @incmod.add_attribute(RDoc::Attr.new(nil, "incmod_private_attr", "RW", ""))
1529+
incmod_private_attr.visibility = :private
1530+
1531+
extmod_tl = @store.add_file("extmod.rb")
1532+
@extmod = extmod_tl.add_module(RDoc::NormalModule, "Extmod")
1533+
1534+
extmod_method = @extmod.add_method(RDoc::AnyMethod.new(nil, "extmod_method_without_a_section"))
1535+
extmod_method = @extmod.add_method(RDoc::AnyMethod.new(nil, "extmod_method"))
1536+
extmod_method.section = @extmod.add_section("Extmod method section")
1537+
1538+
extmod_attr = @extmod.add_attribute(RDoc::Attr.new(nil, "extmod_attr_without_a_section", "RW", "", true))
1539+
extmod_attr = @extmod.add_attribute(RDoc::Attr.new(nil, "extmod_attr", "RW", "", true))
1540+
extmod_attr.section = @extmod.add_section("Extmod attr section")
1541+
1542+
extmod_private_method = @extmod.add_method(RDoc::AnyMethod.new(nil, "extmod_private_method"))
1543+
extmod_private_method.visibility = :private
1544+
1545+
extmod_private_attr = @extmod.add_attribute(RDoc::Attr.new(nil, "extmod_private_attr", "RW", "", true))
1546+
extmod_private_attr.visibility = :private
1547+
1548+
@klass.add_include(RDoc::Include.new("Incmod", nil))
1549+
@klass.add_extend(RDoc::Include.new("Extmod", nil))
1550+
1551+
@klass.add_include(RDoc::Include.new("ExternalInclude", nil))
1552+
@klass.add_extend(RDoc::Include.new("ExternalExtend", nil))
1553+
end
1554+
1555+
def test_embed_mixin_when_false_does_not_embed_anything
1556+
assert_false(@klass.options.embed_mixins)
1557+
@klass.complete(:protected)
1558+
1559+
refute_includes(@klass.constants.map(&:name), "INCMOD_CONST")
1560+
refute_includes(@klass.method_list.map(&:name), "incmod_method")
1561+
refute_includes(@klass.method_list.map(&:name), "extmod_method")
1562+
refute_includes(@klass.attributes.map(&:name), "incmod_attr")
1563+
refute_includes(@klass.attributes.map(&:name), "extmod_attr")
1564+
end
1565+
1566+
def test_embed_mixin_when_true_embeds_methods_and_constants
1567+
@klass.options.embed_mixins = true
1568+
@klass.complete(:protected)
1569+
1570+
# assert on presence and identity of methods and constants
1571+
constant = @klass.constants.find { |c| c.name == "INCMOD_CONST" }
1572+
assert(constant, "constant from included mixin should be present")
1573+
assert_equal(@incmod, constant.mixin_from)
1574+
1575+
instance_method = @klass.method_list.find { |m| m.name == "incmod_method" }
1576+
assert(instance_method, "instance method from included mixin should be present")
1577+
refute(instance_method.singleton)
1578+
assert_equal(@incmod, instance_method.mixin_from)
1579+
1580+
instance_attr = @klass.attributes.find { |a| a.name == "incmod_attr" }
1581+
assert(instance_attr, "instance attr from included mixin should be present")
1582+
refute(instance_attr.singleton)
1583+
assert_equal(@incmod, instance_attr.mixin_from)
1584+
1585+
refute(@klass.method_list.find { |m| m.name == "incmod_private_method" })
1586+
refute(@klass.attributes.find { |m| m.name == "incmod_private_attr" })
1587+
1588+
class_method = @klass.method_list.find { |m| m.name == "extmod_method" }
1589+
assert(class_method, "class method from extended mixin should be present")
1590+
assert(class_method.singleton)
1591+
assert_equal(@extmod, class_method.mixin_from)
1592+
1593+
class_attr = @klass.attributes.find { |a| a.name == "extmod_attr" }
1594+
assert(class_attr, "class attr from extended mixin should be present")
1595+
assert(class_attr.singleton)
1596+
assert_equal(@extmod, class_attr.mixin_from)
1597+
1598+
refute(@klass.method_list.find { |m| m.name == "extmod_private_method" })
1599+
refute(@klass.attributes.find { |m| m.name == "extmod_private_attr" })
1600+
1601+
# assert that sections are also imported
1602+
constant_section = @klass.sections.find { |s| s.title == "Incmod const section" }
1603+
assert(constant_section, "constant from included mixin should have a section")
1604+
assert_equal(constant_section, constant.section)
1605+
1606+
instance_method_section = @klass.sections.find { |s| s.title == "Incmod method section" }
1607+
assert(instance_method_section, "instance method from included mixin should have a section")
1608+
assert_equal(instance_method_section, instance_method.section)
1609+
1610+
instance_attr_section = @klass.sections.find { |s| s.title == "Incmod attr section" }
1611+
assert(instance_attr_section, "instance attr from included mixin should have a section")
1612+
assert_equal(instance_attr_section, instance_attr.section)
1613+
1614+
class_method_section = @klass.sections.find { |s| s.title == "Extmod method section" }
1615+
assert(class_method_section, "class method from extended mixin should have a section")
1616+
assert_equal(class_method_section, class_method.section)
1617+
1618+
class_attr_section = @klass.sections.find { |s| s.title == "Extmod attr section" }
1619+
assert(class_attr_section, "class attr from extended mixin should have a section")
1620+
assert_equal(class_attr_section, class_attr.section)
1621+
1622+
# and check that code objects without a section still have no section
1623+
constant = @klass.constants.find { |c| c.name == "INCMOD_CONST_WITHOUT_A_SECTION" }
1624+
assert_nil(constant.section.title)
1625+
1626+
instance_method = @klass.method_list.find { |c| c.name == "incmod_method_without_a_section" }
1627+
assert_nil(instance_method.section.title)
1628+
1629+
instance_attr = @klass.attributes.find { |c| c.name == "incmod_attr_without_a_section" }
1630+
assert_nil(instance_attr.section.title)
1631+
1632+
class_method = @klass.method_list.find { |c| c.name == "extmod_method_without_a_section" }
1633+
assert_nil(class_method.section.title)
1634+
1635+
class_attr = @klass.attributes.find { |c| c.name == "extmod_attr_without_a_section" }
1636+
assert_nil(class_attr.section.title)
1637+
end
1638+
end
15031639
end

test/rdoc/test_rdoc_options.rb

+15
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def test_to_yaml
6565
expected = {
6666
'charset' => 'UTF-8',
6767
'encoding' => encoding,
68+
'embed_mixins' => false,
6869
'exclude' => %w[~\z \.orig\z \.rej\z \.bak\z \.gemspec\z],
6970
'hyperlink_all' => false,
7071
'line_numbers' => false,
@@ -589,6 +590,20 @@ def test_parse_root
589590
assert_includes @options.rdoc_include, @options.root.to_s
590591
end
591592

593+
def test_parse_embed_mixins
594+
assert_false(@options.embed_mixins)
595+
596+
out, err = capture_output { @options.parse(["--embed-mixins"]) }
597+
assert_empty(out)
598+
assert_empty(err)
599+
assert_true(@options.embed_mixins)
600+
601+
out, err = capture_output { @options.parse(["--no-embed-mixins"]) }
602+
assert_empty(out)
603+
assert_empty(err)
604+
assert_false(@options.embed_mixins)
605+
end
606+
592607
def test_parse_tab_width
593608
@options.parse %w[--tab-width=1]
594609
assert_equal 1, @options.tab_width

0 commit comments

Comments
 (0)