Summary
The ObjC extractor builds its nodes by hand and never sets _callable or _callable_class, so no ObjC declaration is visible to any pass that indexes declarations by those markers. Every other language gets both from one place in the generic engine.
graphify/extractors/objc.py:
def add_node(nid: str, label: str, line: int) -> None:
if nid not in seen_ids:
seen_ids.add(nid)
nodes.append({"id": nid, "label": label, "file_type": "code",
"source_file": str_path, "source_location": f"L{line}"})
versus graphify/extractors/engine.py, which stamps them for every generic-engine language:
for n in nodes:
if n["id"] in callable_def_nids:
n["_callable"] = True
if n["id"] in callable_class_nids:
n["_callable_class"] = True
Reproduction
// Greeter.h
@interface Greeter : NSObject
- (void)greet;
@end
$ python -c "...extract(['Greeter.h'])..."
NODE 'Greeter.h' _callable=None _callable_class=None # file node, correct
NODE 'Greeter' _callable=None _callable_class=None # <- should be a type
NODE 'NSObject' _callable=None _callable_class=None # dangling stub, correct
NODE '-greet' _callable=None _callable_class=None # <- should be callable
The same class written in Java:
NODE 'Greeting' _callable=True _callable_class=True
NODE '.greet()' _callable=True _callable_class=None
Why it matters
_callable_class + source_file is the standard "this is a real type declaration, not a stub" gate. Consumers today:
Marker-gated passes are the pattern for anything that needs to distinguish a declaration from a reference, so this is a floor that every new cross-file or merge-time pass hits with ObjC.
A second, related gap: the method-label sigil
ObjC method labels keep the selector sigil (objc.py, add_node(method_nid, f"{prefix}{method_name}", line)), so the label is -greet / +shared where every other extractor emits .greet(). Any pass that builds a name key from a label by stripping . and () — the normalizer shape used by the merge-time passes — gets -greet and never matches a plain greet from a call site.
The two together are what keep ObjC out of the merge-time cross-repo passes: the marker gap makes the class invisible to the declaration index, and the sigil makes its methods unmatchable by name.
Scope of the attached PR
The PR pairs with this issue and fixes the marker half only: @interface / @implementation / @protocol nodes get _callable + _callable_class, method nodes get _callable, and the file node and dangling-reference stubs stay unmarked. That half stands alone and is a prerequisite for the other.
The sigil half touches name normalization rather than the extractor, and the natural place for it is whichever normalizer is doing the matching, so I left it out of this PR rather than guess at the shape. Happy to follow up with it — say which side you'd rather have it on (extractor label vs. normalizer).
Summary
The ObjC extractor builds its nodes by hand and never sets
_callableor_callable_class, so no ObjC declaration is visible to any pass that indexes declarations by those markers. Every other language gets both from one place in the generic engine.graphify/extractors/objc.py:versus
graphify/extractors/engine.py, which stamps them for every generic-engine language:Reproduction
The same class written in Java:
Why it matters
_callable_class+source_fileis the standard "this is a real type declaration, not a stub" gate. Consumers today:cross_repo_types.py:42— thesame_type_aspass from merge-graphs: a contract type both repos declare stays two unconnected nodes #3007: no ObjC class can ever be a candidate.extract.py:4211andextract.py:4320— the qualified-name declaration indexes.extract.py:6904—class_nids, the indirect_call callable guard admits classes → false edges for select(Model), except tuples, and getattr literals #2137 exclusion that keeps a class from being resolved as a callback target. ObjC currently lands on the right side of this one by accident, because it has no_callableeither.cli.py:3786/watch.py:1644— both carry the markers across an incremental rebuild, so the gap is preserved rather than healed on the next run.Marker-gated passes are the pattern for anything that needs to distinguish a declaration from a reference, so this is a floor that every new cross-file or merge-time pass hits with ObjC.
A second, related gap: the method-label sigil
ObjC method labels keep the selector sigil (
objc.py,add_node(method_nid, f"{prefix}{method_name}", line)), so the label is-greet/+sharedwhere every other extractor emits.greet(). Any pass that builds a name key from a label by stripping.and()— the normalizer shape used by the merge-time passes — gets-greetand never matches a plaingreetfrom a call site.The two together are what keep ObjC out of the merge-time cross-repo passes: the marker gap makes the class invisible to the declaration index, and the sigil makes its methods unmatchable by name.
Scope of the attached PR
The PR pairs with this issue and fixes the marker half only:
@interface/@implementation/@protocolnodes get_callable+_callable_class, method nodes get_callable, and the file node and dangling-reference stubs stay unmarked. That half stands alone and is a prerequisite for the other.The sigil half touches name normalization rather than the extractor, and the natural place for it is whichever normalizer is doing the matching, so I left it out of this PR rather than guess at the shape. Happy to follow up with it — say which side you'd rather have it on (extractor label vs. normalizer).