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: @interface → inherits 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) |
✅ |
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:
Expected:
@interface→inheritsedges,[self method]→callsedges,self.product.name→ property access edges,addTarget:action:→ callback edges,@property NSArray<X*> *→referencesedges,+classMethodlabeled correctly.Actual: 37 nodes, 30 edges, 11 communities — but all
callsedges 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:
.hfiles dispatched to C extractor instead of ObjC extractorRoot cause:
_DISPATCHmapping atextract.py:12210:Impact: Every
.hfile in an ObjC project is parsed byextract_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
.hfile:@interface Foo : Bar→ noinheritsedge<Protocol>conformance → noimplementsedge@property (strong) SomeType *prop→ noreferencesedgeVerification:
Suggested fix: In the dispatch logic, detect ObjC
.hfiles (by@interfacecontent heuristic, or when sibling.m/.mmfiles exist) and route them toextract_objc.Bug 2:
walk_callsnever matches — 0callsedges for all ObjC projectsRoot cause:
walk_callsatextract.py:9403-9427looks forselectorandkeyword_argument_listchildren insidemessage_expressionnodes:But tree-sitter-objc represents simple selectors as
identifier, notselector:Only compound selectors like
[self tableView:tv cellForRowAtIndexPath:ip]producekeyword_argument_list.Impact: Every
[receiver selector]call in every.mfile is invisible. The entire method-body second pass (lines 9399–9427) is effectively dead code for ObjC. Nocallsedges are ever produced.Verification: Extracted 4
.mfiles with 11 method definitions, 0callsedges. Confirmed[self setupUI],[self loadProducts],[self setupTableView],[Foo sharedInstance],[super viewDidLoad]all produceidentifierselector nodes.Suggested fix: Add
"identifier"to the child type check, distinguishing receiver from selector (skip the firstidentifier, or check field names if tree-sitter-objc provides them):Bug 3: Generic/parameterized property types not extracted
Root cause:
property_declarationhandler atextract.py:9323-9332only looks fortype_identifieras a direct child ofstruct_declaration:But tree-sitter-objc wraps parameterized types in a
generic_specifiernode:Impact: Foundation generic collections (
NSArray<X*>,NSDictionary<K,V*>,NSSet<X*>) and custom generic ObjC types are invisible. Only non-generic property types producereferencesedges.Verification:
Suggested fix: Also recurse into
generic_specifierchildren when walkingstruct_declarationchildren.Bug 4: Class methods (
+) labeled as instance methods (-)Root cause:
method_definitionhandler atextract.py:9388hardcodes the label prefix:Impact:
+ (instancetype)sharedInstancebecomes-sharedInstancein the graph. Node IDs andmethodedges 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:Bug 5: Method-body property access (
self.product.name) not extractedRoot cause:
walk_calls(extract.py:9403) only handlesmessage_expressionnodes. ObjC dot-syntax property access is afield_expression— a completely different AST node type with no handler anywhere in the extractor.tree-sitter-objc produces for
self.product.name: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@propertydeclaration (Bug 1/3) appears in the graph, not the actual usage.Suggested fix: Add a
field_expressionhandler in the method-body walker (similar towalk_calls) that emitsaccessesedges from caller method → field identifier → resolved type.Bug 6: Compound message expressions +
@selector()not capturedRoot cause (6a): tree-sitter-objc flattens compound ObjC messages. Unlike simple
[self foo]which wraps parts in amessage_expressionnode, compound messages like[obj method:arg1 param2:arg2]have brackets and parts as direct siblings of the parent with no wrapper:Impact (6a):
walk_callsonly visitsmessage_expressionnodes → 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 aselector_expressionnode 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:
Suggested fix for 6a: Instead of only traversing
message_expression, also handle ObjC[/]bracket pairs directly as compound message boundaries. Walkcompound_statementchildren for bracket-delimited message sequences.Suggested fix for 6b: Add a
selector_expressionhandler that extracts the method name from@selector(xxx)and emits aregisters-callbackortarget-actionedge to the referenced method.Summary of uncovered relationships
@interface Foo : Barin .hinherits<Protocol>in .himplements@property X *pin .hreferences[self method]/[Foo method]calls[obj method:arg1 param2:arg2]calls[[Foo alloc] init]instantiatesself.product.nameaccessesaddTarget:action:@selector(xxx)target-action@property NSArray<X*> *preferences@property NSDictionary<K,V*> *preferences+ (void)classMethodmethod(label wrong)@interface Foo : Barin .minherits@property X *pin .m extensionreferences<Protocol>in .m extensionimplements#import "X.h"imports@implementation Foocontains- (void)methoddefinitionmethod(label correct)