Skip to content

ObjC declarations carry no _callable / _callable_class markers, so marker-gated passes skip ObjC entirely #3228

Description

@xiongjianxu

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions