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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions