`_merge_decl_def_classes` folds an @interface/@implementation pair into one node
only when both live in the SAME directory (the id embeds the directory path,
#1556). A class whose `.h` sits in `include/` and `.m` in `src/` therefore mints
two same-labelled nodes that never merge, so `[w render]` saw two type-def
candidates, tripped the member-call resolver's single-definition god-node guard
(`if len(type_defs) != 1: continue`), and emitted NO edge -- the same corpus
resolved fine when the pair shared a directory. Header/impl directory splits are
common layout, so this hit ordinary code.
Replace the three per-arm god-node bails with a shared `_resolve_member_target`
that unions the candidates' members and binds the selector to the sole candidate
that DEFINES it (an @implementation in `.m`/`.mm`, or a merged node carrying
`definition_file`). A header-only node is a bare declaration and does not count,
so the split pair resolves to its one real definition. Two unrelated same-named
classes that each define the selector still yield two definitions and stay
ambiguous -- the guard holds for the genuinely-ambiguous case it was meant for.
Co-Authored-By: Claude Code <noreply@anthropic.com>
(cherry picked from commit 510934c686)
#2589 (PR #2500): a @protocol declaration (labeled <Name>) is excluded from
the receiver-type index, so it no longer collides with a same-named class.
#2590 (PR #2501): a category/class-extension interface is keyed off the
base stem and folds into the base class instead of minting a duplicate
node.
#2591 (fresh): @property and ivar declarations are captured into a
per-class field-type table, and a message send to a self.field / _ivar
receiver resolves through it (bare field name only, so Foo.shared cannot
fabricate to a FooShared class). All hold the single-definition guard and
emit INFERRED.
Adapts PRs #2500/#2501 (thanks @xiongjianxu); #2591 fresh.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>