diff --git a/lib/rdoc/class_module.rb b/lib/rdoc/class_module.rb index 7609080fbf..3d94d78698 100644 --- a/lib/rdoc/class_module.rb +++ b/lib/rdoc/class_module.rb @@ -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 @@ -798,5 +799,41 @@ def update_extends extends.uniq! end -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) + self.visibility = code_object.visibility + code_object + end +end diff --git a/lib/rdoc/code_object.rb b/lib/rdoc/code_object.rb index aeb4b4762e..0dac6a9387 100644 --- a/lib/rdoc/code_object.rb +++ b/lib/rdoc/code_object.rb @@ -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 diff --git a/lib/rdoc/generator/template/darkfish/class.rhtml b/lib/rdoc/generator/template/darkfish/class.rhtml index 97d175dddc..46d0b3949c 100644 --- a/lib/rdoc/generator/template/darkfish/class.rhtml +++ b/lib/rdoc/generator/template/darkfish/class.rhtml @@ -53,7 +53,13 @@ <%- constants.each do |const| -%>
<%= const.name %> <%- if const.comment then -%> -
<%= const.description.strip %> +
+ <%- if const.mixin_from then -%> +
+ Included from <%= const.mixin_from.full_name %> +
+ <%- end -%> + <%= const.description.strip %> <%- else -%>
(Not documented) <%- end -%> @@ -76,6 +82,11 @@
+ <%- if attrib.mixin_from then -%> +
+ <%= attrib.singleton ? "Extended" : "Included" %> from <%= attrib.mixin_from.full_name %> +
+ <%- end -%> <%- if attrib.comment then -%> <%= attrib.description.strip %> <%- else -%> @@ -124,6 +135,11 @@
+ <%- if method.mixin_from then -%> +
+ <%= method.singleton ? "Extended" : "Included" %> from <%= method.mixin_from.full_name %> +
+ <%- end -%> <%- if method.comment then -%> <%= method.description.strip %> <%- else -%> diff --git a/lib/rdoc/generator/template/darkfish/css/rdoc.css b/lib/rdoc/generator/template/darkfish/css/rdoc.css index ddaf4d47c6..8ac9761755 100644 --- a/lib/rdoc/generator/template/darkfish/css/rdoc.css +++ b/lib/rdoc/generator/template/darkfish/css/rdoc.css @@ -586,6 +586,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; } diff --git a/lib/rdoc/options.rb b/lib/rdoc/options.rb index 6ee29fd071..6ff8357dda 100644 --- a/lib/rdoc/options.rb +++ b/lib/rdoc/options.rb @@ -343,6 +343,11 @@ 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 @@ -350,6 +355,7 @@ def initialize loaded_options = nil # :nodoc: def init_ivars # :nodoc: @dry_run = false + @embed_mixins = false @exclude = %w[ ~\z \.orig\z \.rej\z \.bak\z \.gemspec\z @@ -400,6 +406,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'] @@ -431,6 +438,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') @@ -459,11 +467,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 @@ -841,6 +850,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, diff --git a/test/rdoc/test_rdoc_class_module.rb b/test/rdoc/test_rdoc_class_module.rb index 27d37cb7dd..87d9b276d3 100644 --- a/test/rdoc/test_rdoc_class_module.rb +++ b/test/rdoc/test_rdoc_class_module.rb @@ -1500,4 +1500,132 @@ 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 + + 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 + + @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" }) + + 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" }) + + # 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 diff --git a/test/rdoc/test_rdoc_options.rb b/test/rdoc/test_rdoc_options.rb index 443d595ca6..67210258d5 100644 --- a/test/rdoc/test_rdoc_options.rb +++ b/test/rdoc/test_rdoc_options.rb @@ -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, @@ -590,6 +591,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