diff --git a/lib/rdoc/code_object/class_module.rb b/lib/rdoc/code_object/class_module.rb
index b564059ae7..de7665a589 100644
--- a/lib/rdoc/code_object/class_module.rb
+++ b/lib/rdoc/code_object/class_module.rb
@@ -673,7 +673,7 @@ def path
# module or class return the name of the latter.
def name_for_path
- is_alias_for ? is_alias_for.full_name : full_name
+ is_alias_for ? is_alias_for.name_for_path : full_name
end
##
@@ -848,6 +848,13 @@ def type
def update_aliases
constants.each do |const|
next unless cm = const.is_alias_for
+
+ # Resolve chained aliases (A = B = C) to the real class/module.
+ cm = @store.find_class_or_module(cm.full_name) || cm
+ while (target = cm.is_alias_for)
+ cm = target
+ end
+
cm_alias = cm.dup
cm_alias.name = const.name
diff --git a/lib/rdoc/markup/to_html_crossref.rb b/lib/rdoc/markup/to_html_crossref.rb
index 106810072b..a0e218b657 100644
--- a/lib/rdoc/markup/to_html_crossref.rb
+++ b/lib/rdoc/markup/to_html_crossref.rb
@@ -59,21 +59,24 @@ def init_link_notation_regexp_handlings
# given it is used as the link text, otherwise +name+ is used.
def cross_reference(name, text = nil, code = true, rdoc_ref: false)
- lookup = name
+ # What to show when the reference doesn't resolve to a link:
+ # caller-provided text if any, otherwise the original name (preserving '#').
+ fallback = text || name
- name = name[1..-1] unless @show_hash if name[0, 1] == '#'
+ # Strip '#' for link display text (e.g. #method shows as "method" in links)
+ display = !@show_hash && name.start_with?('#') ? name[1..] : name
- if !name.end_with?('+@', '-@') && match = name.match(/(.*[^#:])?@(.*)/)
+ if !display.end_with?('+@', '-@') && match = display.match(/(.*[^#:])?@(.*)/)
context_name = match[1]
label = RDoc::Text.decode_legacy_label(match[2])
text ||= "#{label} at #{context_name}" if context_name
text ||= label
code = false
else
- text ||= name
+ text ||= display
end
- link lookup, text, code, rdoc_ref: rdoc_ref
+ link(name, text, code, rdoc_ref: rdoc_ref) || fallback
end
##
@@ -150,6 +153,7 @@ def gen_url(url, text)
##
# Creates an HTML link to +name+ with the given +text+.
+ # Returns the link HTML string, or +nil+ if the reference could not be resolved.
def link(name, text, code = true, rdoc_ref: false)
if !(name.end_with?('+@', '-@')) and name =~ /(.*[^#:])?@/
@@ -162,56 +166,60 @@ def link(name, text, code = true, rdoc_ref: false)
# Non-text source files (C, Ruby, etc.) don't get HTML pages generated,
# so don't auto-link to them. Explicit rdoc-ref: links are still allowed.
if !rdoc_ref && RDoc::TopLevel === ref && !ref.text?
- return text
+ return
end
case ref
- when String then
+ when String
if rdoc_ref && @warn_missing_rdoc_ref
puts "#{@from_path}: `rdoc-ref:#{name}` can't be resolved for `#{text}`"
end
- ref
+ return
+ when nil
+ # A bare label reference like @foo still produces a valid anchor link
+ return unless label
+ path = +""
else
- path = ref ? ref.as_href(@from_path) : +""
+ path = ref.as_href(@from_path)
if code and RDoc::CodeObject === ref and !(RDoc::TopLevel === ref)
text = "#{CGI.escapeHTML text}"
end
+ end
- if label
- # Decode legacy labels (e.g., "What-27s+Here" -> "What's Here")
- # then convert to GitHub-style anchor format
- decoded_label = RDoc::Text.decode_legacy_label(label)
- formatted_label = RDoc::Text.to_anchor(decoded_label)
-
- # Case 1: Path already has an anchor (e.g., method link)
- # Input: C1#method@label -> path="C1.html#method-i-m"
- # Output: C1.html#method-i-m-label
- if path =~ /#/
- path << "-#{formatted_label}"
-
- # Case 2: Label matches a section title
- # Input: C1@Section -> path="C1.html", section "Section" exists
- # Output: C1.html#section (uses section.aref for GitHub-style)
- elsif (section = ref&.sections&.find { |s| decoded_label == s.title })
- path << "##{section.aref}"
-
- # Case 3: Ref has an aref (class/module context)
- # Input: C1@heading -> path="C1.html", ref=C1 class
- # Output: C1.html#class-c1-heading
- elsif ref.respond_to?(:aref)
- path << "##{ref.aref}-#{formatted_label}"
-
- # Case 4: No context, just the label (e.g., TopLevel/file)
- # Input: README@section -> path="README_md.html"
- # Output: README_md.html#section
- else
- path << "##{formatted_label}"
- end
+ if label
+ # Decode legacy labels (e.g., "What-27s+Here" -> "What's Here")
+ # then convert to GitHub-style anchor format
+ decoded_label = RDoc::Text.decode_legacy_label(label)
+ formatted_label = RDoc::Text.to_anchor(decoded_label)
+
+ # Case 1: Path already has an anchor (e.g., method link)
+ # Input: C1#method@label -> path="C1.html#method-i-m"
+ # Output: C1.html#method-i-m-label
+ if path =~ /#/
+ path << "-#{formatted_label}"
+
+ # Case 2: Label matches a section title
+ # Input: C1@Section -> path="C1.html", section "Section" exists
+ # Output: C1.html#section (uses section.aref for GitHub-style)
+ elsif (section = ref&.sections&.find { |s| decoded_label == s.title })
+ path << "##{section.aref}"
+
+ # Case 3: Ref has an aref (class/module context)
+ # Input: C1@heading -> path="C1.html", ref=C1 class
+ # Output: C1.html#class-c1-heading
+ elsif ref.respond_to?(:aref)
+ path << "##{ref.aref}-#{formatted_label}"
+
+ # Case 4: No context, just the label (e.g., TopLevel/file)
+ # Input: README@section -> path="README_md.html"
+ # Output: README_md.html#section
+ else
+ path << "##{formatted_label}"
end
-
- "#{text}"
end
+
+ "#{text}"
end
def handle_TT(code)
diff --git a/test/rdoc/generator/darkfish_test.rb b/test/rdoc/generator/darkfish_test.rb
index bf4ec57058..dc9f7ae035 100644
--- a/test/rdoc/generator/darkfish_test.rb
+++ b/test/rdoc/generator/darkfish_test.rb
@@ -610,6 +610,43 @@ def test_canonical_url_for_classes
assert_include(content, '')
end
+ def test_generate_chained_alias_sidebar_links
+ # Reproduces ruby/rdoc#1664:
+ # class Original < Base
+ # DirectAlias = Original # alias of real class
+ # ChainedAlias = DirectAlias # alias of an alias
+ #
+ # ChainedAlias's sidebar link must point to Original.html, not DirectAlias.html
+ # (which is never generated because aliases don't get their own files).
+ parent = @top_level.add_module RDoc::NormalModule, 'Parent'
+ original = parent.add_class RDoc::NormalClass, 'Original'
+
+ direct_alias_const = RDoc::Constant.new 'DirectAlias', nil, ''
+ direct_alias_const.record_location @top_level
+ direct_alias_const.is_alias_for = original
+ parent.add_constant direct_alias_const
+ parent.update_aliases
+
+ direct_alias = @store.find_class_or_module 'Parent::DirectAlias'
+
+ chained_alias_const = RDoc::Constant.new 'ChainedAlias', nil, ''
+ chained_alias_const.record_location @top_level
+ chained_alias_const.is_alias_for = direct_alias
+ parent.add_constant chained_alias_const
+ parent.update_aliases
+
+ @store.complete :private
+ @g.generate
+
+ assert_file 'Parent/Original.html'
+ refute File.exist?('Parent/DirectAlias.html'), 'alias should not get its own file'
+ refute File.exist?('Parent/ChainedAlias.html'), 'chained alias should not get its own file'
+
+ index_html = File.binread('index.html')
+ assert_match %r{href="\./Parent/Original\.html">DirectAlias<}, index_html
+ assert_match %r{href="\./Parent/Original\.html">ChainedAlias<}, index_html
+ end
+
def test_canonical_url_for_rdoc_files
@store.add_file("CONTRIBUTING.rdoc", parser: RDoc::Parser::Simple)
diff --git a/test/rdoc/markup/to_html_crossref_test.rb b/test/rdoc/markup/to_html_crossref_test.rb
index ff243fc9e0..24e0c97e86 100644
--- a/test/rdoc/markup/to_html_crossref_test.rb
+++ b/test/rdoc/markup/to_html_crossref_test.rb
@@ -407,7 +407,7 @@ def test_convert_RDOCLINK_rdoc_ref_c_file_linked
end
def test_link
- assert_equal 'n', @to.link('n', 'n')
+ assert_nil @to.link('n', 'n')
assert_equal 'm', @to.link('m', 'm')
end
@@ -423,6 +423,20 @@ def test_link_class_method_full
@to.link('Parent::m', 'Parent::m')
end
+ def test_handle_regexp_CROSSREF_hash_preserved_for_unresolved
+ @to.show_hash = false
+
+ # #no should not lose its '#' when it doesn't resolve to a method
+ assert_equal "#no", REGEXP_HANDLING('#no')
+ end
+
+ def test_cross_reference_preserves_explicit_text_for_unresolved
+ # When explicit text is provided, it should be preserved on unresolved refs
+ assert_equal "Foo", @to.cross_reference("Missing", "Foo")
+ end
+
+ private
+
def para(text)
"\n
#{text}
\n" end