Skip to content

Bug: ObjC extractor — 4 bugs causing ~60% of relationships to be silently dropped #1475

Description

@JabberYQ

Summary

Testing /graphify . on a pure Objective-C iOS project (12 files, ~1,125 words) revealed 6 bugs in the ObjC extractor that collectively cause ~60% of code-level relationships to be silently dropped.

Test repo structure:

Demo/
  AppDelegate.h/.m
  Product.h/.m
  ProductListViewController.h/.m
  ProductDetailViewController.h/.m
  AlertManager.h/.m
  main.m

Expected: @interfaceinherits edges, [self method]calls edges, self.product.name → property access edges, addTarget:action: → callback edges, @property NSArray<X*> *references edges, +classMethod labeled correctly.
Actual: 37 nodes, 30 edges, 11 communities — but all calls edges are missing, .h files are extracted as C (0 ObjC edges), property accesses & target-action are invisible, generic property types are invisible, and class methods are mislabeled.


Bug 1: .h files dispatched to C extractor instead of ObjC extractor

Root cause: _DISPATCH mapping at extract.py:12210:

".h": extract_c,     # ← ObjC headers parsed as C!
".m": extract_objc,
".mm": extract_objc,

Impact: Every .h file in an ObjC project is parsed by extract_c, which doesn't understand @interface, @property, @protocol, or method declarations. The C extractor produces exactly 1 node (the file itself) and 0 edges.

What's lost per .h file:

  • @interface Foo : Bar → no inherits edge
  • <Protocol> conformance → no implements edge
  • @property (strong) SomeType *prop → no references edge
  • Method declarations → no nodes

Verification:

# extract_c on AppDelegate.h → 1 node, 0 edges
# extract_objc on AppDelegate.h → 5 nodes, 5 edges
#   (AppDelegate, UIResponder, UIApplicationDelegate, UIWindow
#    + inherits/implements/references edges)

Suggested fix: In the dispatch logic, detect ObjC .h files (by @interface content heuristic, or when sibling .m/.mm files exist) and route them to extract_objc.


Bug 2: walk_calls never matches — 0 calls edges for all ObjC projects

Root cause: walk_calls at extract.py:9403-9427 looks for selector and keyword_argument_list children inside message_expression nodes:

if child.type in ("selector", "keyword_argument_list"):

But tree-sitter-objc represents simple selectors as identifier, not selector:

# AST for [self setupUI]:
message_expression
  [                   "["
  identifier          "self"
  identifier          "setupUI"    ← type is "identifier", not "selector"
  ]                   "]"

Only compound selectors like [self tableView:tv cellForRowAtIndexPath:ip] produce keyword_argument_list.

Impact: Every [receiver selector] call in every .m file is invisible. The entire method-body second pass (lines 9399–9427) is effectively dead code for ObjC. No calls edges are ever produced.

Verification: Extracted 4 .m files with 11 method definitions, 0 calls edges. Confirmed [self setupUI], [self loadProducts], [self setupTableView], [Foo sharedInstance], [super viewDidLoad] all produce identifier selector nodes.

Suggested fix: Add "identifier" to the child type check, distinguishing receiver from selector (skip the first identifier, or check field names if tree-sitter-objc provides them):

if child.type in ("selector", "identifier", "keyword_argument_list"):

Bug 3: Generic/parameterized property types not extracted

Root cause: property_declaration handler at extract.py:9323-9332 only looks for type_identifier as a direct child of struct_declaration:

for s in sub.children:
    if s.type == "type_identifier":   # ← direct child only

But tree-sitter-objc wraps parameterized types in a generic_specifier node:

# @property NSArray<Product *> *products:
struct_declaration
  generic_specifier              ← NOT type_identifier!
    type_identifier: "NSArray"   ← nested one level deeper
    <
    type_name: "Product *"
    >
  struct_declarator: "*products"

Impact: Foundation generic collections (NSArray<X*>, NSDictionary<K,V*>, NSSet<X*>) and custom generic ObjC types are invisible. Only non-generic property types produce references edges.

Verification:

@property (nonatomic, strong) UITableView *tableView;      // ✅ extracted
@property (nonatomic, strong) NSArray<Product *> *products; // ❌ not extracted

Suggested fix: Also recurse into generic_specifier children when walking struct_declaration children.


Bug 4: Class methods (+) labeled as instance methods (-)

Root cause: method_definition handler at extract.py:9388 hardcodes the label prefix:

add_node(method_nid, f"-{method_name}", line)

Impact: + (instancetype)sharedInstance becomes -sharedInstance in the graph. Node IDs and method edges are correct, but labels are misleading — users cannot distinguish class methods from instance methods.

Suggested fix: Check the first child of the method node. tree-sitter-objc emits + or - as the first child:

prefix = "-"
for child in node.children:
    if child.type == "+":
        prefix = "+"
        break
add_node(method_nid, f"{prefix}{method_name}", line)

Bug 5: Method-body property access (self.product.name) not extracted

Root cause: walk_calls (extract.py:9403) only handles message_expression nodes. ObjC dot-syntax property access is a field_expression — a completely different AST node type with no handler anywhere in the extractor.

tree-sitter-objc produces for self.product.name:

assignment_expression
  field_expression: "self.product.name"
    field_expression: "self.product"
      identifier: "self"
      .: "."
      field_identifier: "product"
    .: "."
    field_identifier: "name"
  =: "="
  string_literal: "@\"hi\""

Impact: All property read/write via dot syntax inside method bodies is invisible. This includes self.product.name, self.view.backgroundColor, self.tableView.rowHeight — every property access that connects ViewControllers to their data and views. Only the static @property declaration (Bug 1/3) appears in the graph, not the actual usage.

Suggested fix: Add a field_expression handler in the method-body walker (similar to walk_calls) that emits accesses edges from caller method → field identifier → resolved type.


Bug 6: Compound message expressions + @selector() not captured

Root cause (6a): tree-sitter-objc flattens compound ObjC messages. Unlike simple [self foo] which wraps parts in a message_expression node, compound messages like [obj method:arg1 param2:arg2] have brackets and parts as direct siblings of the parent with no wrapper:

# Simple [self foo] — HAS message_expression wrapper:
expression_statement
  message_expression      ← wrapper exists
    [                     "["
    identifier            "self"
    identifier            "foo"
    ]                     "]"

# Compound [b addTarget:self action:@selector(onTap) forControlEvents:0]:
# NO message_expression wrapper at all!
[                         ← direct child of compound_statement
identifier: "b"
identifier: "addTarget"
:: ":"
identifier: "self"
identifier: "action"
:: ":"
selector_expression: "@selector(onTap)"
identifier: "forControlEvents"
:: ":"
number_literal: "0"
]                         "]"

Impact (6a): walk_calls only visits message_expression nodes → compound messages are never processed. Even if Bug 2 is fixed (adding "identifier" to the check), compound messages remain invisible because the parent node doesn't exist.

Root cause (6b): @selector(onTap) produces a selector_expression node with no handler. This is the mechanism for target-action, delegate callbacks, and notification handlers — the backbone of Cocoa/CocoaTouch event wiring.

Impact (6b): Target-action bindings (addTarget:action:), timer callbacks, gesture recognizer handlers, and notification selectors are all invisible. The graph can't trace UI event flow from button tap → handler method.

Verification:

// None of these produce any edges:
[self.buyButton addTarget:self action:@selector(onBuyButtonTapped) ...]
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) ...]
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(update) ...]

Suggested fix for 6a: Instead of only traversing message_expression, also handle ObjC [/] bracket pairs directly as compound message boundaries. Walk compound_statement children for bracket-delimited message sequences.

Suggested fix for 6b: Add a selector_expression handler that extracts the method name from @selector(xxx) and emits a registers-callback or target-action edge to the referenced method.


Summary of uncovered relationships

Source code pattern Expected edge Extracted?
@interface Foo : Bar in .h inherits ❌ (Bug 1)
<Protocol> in .h implements ❌ (Bug 1)
@property X *p in .h references ❌ (Bug 1)
[self method] / [Foo method] calls ❌ (Bug 2)
[obj method:arg1 param2:arg2] calls ❌ (Bug 6a)
[[Foo alloc] init] instantiates ❌ (Bug 2)
self.product.name accesses ❌ (Bug 5)
addTarget:action:@selector(xxx) target-action ❌ (Bug 6a+6b)
@property NSArray<X*> *p references ❌ (Bug 3)
@property NSDictionary<K,V*> *p references ❌ (Bug 3)
+ (void)classMethod method (label wrong) ⚠️ (Bug 4)
@interface Foo : Bar in .m inherits
@property X *p in .m extension references ✅ (non-generic only)
<Protocol> in .m extension implements
#import "X.h" imports
@implementation Foo contains
- (void)method definition method (label correct)

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