Skip to content

Commit 5da426b

Browse files
committed
Fix module recursive lookup bug
1 parent 4b84660 commit 5da426b

File tree

3 files changed

+79
-40
lines changed

3 files changed

+79
-40
lines changed

lib/rdoc/context.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,4 +1261,50 @@ def upgrade_to_class mod, class_type, enclosing
12611261

12621262
autoload :Section, "#{__dir__}/context/section"
12631263

1264+
##
1265+
# Attempts to locate the module object in this context.
1266+
#
1267+
# The scoping rules of Ruby to resolve the name of an included module are:
1268+
# - first search constant directly defined in nested modules from inside to outside
1269+
# - if not found, look into the children of included modules,
1270+
# in reverse inclusion order;
1271+
# - if still not found, look into included modules of Object
1272+
1273+
def lookup_module(name, before: nil, searched: {}.compare_by_identity)
1274+
# Search nested modules first
1275+
nesting = self
1276+
while nesting
1277+
full_name = nesting.child_name(name)
1278+
mod = @store.modules_hash[full_name]
1279+
return mod if mod
1280+
nesting = nesting.parent
1281+
end
1282+
1283+
# Search included modules recursively
1284+
mod = find_module(name, before: before, searched: searched)
1285+
return mod if mod
1286+
1287+
# Search Object recursively
1288+
top_level.object_class.find_module(name, searched: searched)
1289+
end
1290+
1291+
##
1292+
# Recursively search for a module in this context and its includes.
1293+
1294+
def find_module(name, before: nil, searched: {}.compare_by_identity)
1295+
return if searched.include?(self)
1296+
searched[self] = true
1297+
full_name = child_name(name)
1298+
mod = @store.modules_hash[full_name]
1299+
return mod if mod
1300+
1301+
# recursively search the includes in reverse order
1302+
includes.take_while { |i| i != before }.reverse_each do |i|
1303+
inc = i.module
1304+
next if String === inc
1305+
mod = inc.find_module(name, searched: searched)
1306+
return mod if mod
1307+
end
1308+
nil
1309+
end
12641310
end

lib/rdoc/mixin.rb

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -59,49 +59,16 @@ def inspect # :nodoc:
5959
# Attempts to locate the included module object. Returns the name if not
6060
# known.
6161
#
62-
# The scoping rules of Ruby to resolve the name of an included module are:
63-
# - first look into the children of the current context;
64-
# - if not found, look into the children of included modules,
65-
# in reverse inclusion order;
66-
# - if still not found, go up the hierarchy of names.
67-
#
68-
# This method has <code>O(n!)</code> behavior when the module calling
69-
# include is referencing nonexistent modules. Avoid calling #module until
70-
# after all the files are parsed. This behavior is due to ruby's constant
71-
# lookup behavior.
72-
#
73-
# As of the beginning of October, 2011, no gem includes nonexistent modules.
62+
# Avoid calling #module until after all the files are parsed.
63+
# This behavior is due to ruby's constant lookup behavior.
7464

7565
def module
7666
return @module if @module
67+
return @module = @name unless parent
68+
return @module = @name if @name.start_with?('::')
7769

78-
# search the current context
79-
return @name unless parent
80-
full_name = parent.child_name(@name)
81-
@module = @store.modules_hash[full_name]
82-
return @module if @module
83-
return @name if @name =~ /^::/
84-
85-
# search the includes before this one, in reverse order
86-
searched = parent.includes.take_while { |i| i != self }.reverse
87-
searched.each do |i|
88-
inc = i.module
89-
next if String === inc
90-
full_name = inc.child_name(@name)
91-
@module = @store.modules_hash[full_name]
92-
return @module if @module
93-
end
94-
95-
# go up the hierarchy of names
96-
up = parent.parent
97-
while up
98-
full_name = up.child_name(@name)
99-
@module = @store.modules_hash[full_name]
100-
return @module if @module
101-
up = up.parent
102-
end
103-
104-
@name
70+
# search the includes before this one
71+
@module = parent.lookup_module(@name, before: self) || @name
10572
end
10673

10774
##

test/rdoc/test_rdoc_include.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def test_module_extended
6363
m1_k1.add_include i1_k0_m4
6464

6565
assert_equal [i1_m1, i1_m2, i1_m3, i1_m4, i1_k0_m4], m1_k1.includes
66-
assert_equal [m1_m2_k0_m4, m1_m2_m3_m4, m1_m2_m3, m1_m2, m1, @object,
66+
assert_equal [m1_m2_k0_m4, m1_m2_m4, m1_m3, m1_m2, m1, @object,
6767
'BasicObject'], m1_k1.ancestors
6868

6969
m1_k2 = m1.add_class RDoc::NormalClass, 'Klass2'
@@ -96,6 +96,32 @@ def test_module_extended
9696
assert_equal [m1_m2_m4, m1_m2, m1, @object, 'BasicObject'], m1_k3.ancestors
9797
end
9898

99+
def test_include_through_include
100+
top_level = @store.add_file 'file.rb'
101+
102+
mod1 = top_level.add_module RDoc::NormalModule, 'Mod1'
103+
mod2 = top_level.add_module RDoc::NormalModule, 'Mod2'
104+
mod3 = top_level.add_module RDoc::NormalModule, 'Mod3'
105+
submod = mod1.add_module RDoc::NormalModule, 'Sub'
106+
mod2.add_include RDoc::Include.new('Mod1', '')
107+
mod3.add_include RDoc::Include.new('Mod2', '')
108+
mod3.add_include RDoc::Include.new('Sub', '')
109+
assert_equal [submod, mod2], mod3.ancestors
110+
end
111+
112+
def test_include_through_top_level_include
113+
top_level = @store.add_file 'file.rb'
114+
115+
mod1 = top_level.add_module RDoc::NormalModule, 'Mod1'
116+
mod2 = top_level.add_module RDoc::NormalModule, 'Mod2'
117+
mod3 = mod2.add_module RDoc::NormalModule, 'Mod3'
118+
submod = mod1.add_module RDoc::NormalModule, 'Sub'
119+
mod2.add_include RDoc::Include.new('Mod1', '')
120+
top_level.add_include RDoc::Include.new('Mod2', '')
121+
mod3.add_include RDoc::Include.new('Sub', '')
122+
assert_equal [submod], mod3.ancestors
123+
end
124+
99125
def test_store_equals
100126
incl = RDoc::Include.new 'M', nil
101127
incl.record_location RDoc::TopLevel.new @top_level.name

0 commit comments

Comments
 (0)