Skip to content

Commit 5c483d1

Browse files
authored
Fix false self-referential mixin when bare name matches ancestor namespace (#1672)
# Description When a module like `A::Enumerable` contains `include Enumerable`, YARD's lexical resolver walks up the namespace tree, reaches parent namespace `A`, and finds `A::Enumerable` as the match for the bare name `Enumerable`. This records the module as including itself — producing incorrect documentation and potential infinite recursion in `inheritance_tree`. This is the valid-Ruby case reported in #1116 (comment): #1116 (comment) ```ruby module A end module A::Enumerable include Enumerable # YARD incorrectly resolved this to A::Enumerable itself end ``` **Fix:** In `RegistryResolver#lookup_by_path`, after a direct-path lookup returns a result while searching in a *parent* namespace, skip that result if it equals the original starting namespace. This breaks the false cycle while leaving all other lookups unaffected (e.g., root-level lookups, qualified references). # Completed Tasks - [x] I have read the [Contributing Guide][contrib]. - [x] The pull request is complete (implemented / written). - [x] Git commits have been cleaned up (squash WIP / revert commits). - [x] I wrote tests and ran `bundle exec rake` locally (if code is attached to PR). [contrib]: https://github.com/lsegal/yard/blob/main/CONTRIBUTING.md
2 parents f0833c3 + e4f339e commit 5c483d1

3 files changed

Lines changed: 28 additions & 0 deletions

File tree

lib/yard/registry_resolver.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ def lookup_by_path(path, opts = {})
7575
lexical_lookup = 0
7676
while namespace && !resolved
7777
resolved = lookup_path_direct(namespace, path, type)
78+
# Prevent a bare name from resolving back to the namespace we started
79+
# from when searching through a parent namespace. For example,
80+
# `include Enumerable` inside `A::Enumerable` would walk up to namespace
81+
# `A` and match `A::Enumerable`, creating a false self-referential mixin.
82+
# Only skip when we have already moved to a parent (namespace != orignamespace).
83+
# See https://github.com/lsegal/yard/issues/1116
84+
resolved = nil if resolved.equal?(orignamespace) && !namespace.equal?(orignamespace)
7885
resolved ||= lookup_path_inherited(namespace, path, type) if inheritance
7986
break if resolved
8087
namespace = namespace.parent

spec/handlers/examples/mixin_handler_001.rb.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ end
3939
module FromConstant; end
4040
FromConstant.include A
4141

42+
module Outer1
43+
end
44+
45+
module Outer1::Inner
46+
include Inner
47+
end
48+
4249
module Foo
4350
end
4451

spec/handlers/mixin_handler_spec.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@
4545
expect(P("ABC::DEF::BAR").mixins).to eq [P("ABC::BAR")]
4646
end
4747

48+
it "does not create self-referential mixin when bare name matches an ancestor namespace" do
49+
# `include Inner` inside `Outer1::Inner` should not resolve to `Outer1::Inner` itself.
50+
# Previously YARD would walk up to namespace `Outer1` and find `Outer1::Inner`,
51+
# producing a false cyclic mixin. See https://github.com/lsegal/yard/issues/1116
52+
mod = P("Outer1::Inner")
53+
expect(mod.mixins.map(&:path)).not_to include("Outer1::Inner")
54+
expect(mod.mixins.map(&:path)).to eq ["Inner"]
55+
# inheritance_tree(true) should not recurse infinitely and should not
56+
# include Outer1::Inner as a *mixin* (it appears only as self at index 0)
57+
tree = mod.inheritance_tree(true)
58+
expect(tree.first).to eq mod
59+
expect(tree.drop(1).map(&:path)).not_to include("Outer1::Inner")
60+
end
61+
4862
it "raises undocumentable error if argument is variable" do
4963
undoc_error "module X; include invalid; end"
5064
expect(Registry.at('X').mixins).to eq []

0 commit comments

Comments
 (0)