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

feature: Render mixed-in methods and constants with --embed-mixins #842

Merged
merged 4 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
flavorjones marked this conversation as resolved.
Show resolved Hide resolved

##
# 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" })
flavorjones marked this conversation as resolved.
Show resolved Hide resolved
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