fix(objc): fold category/extension interfaces into the base class (#1556) - #2501
fix(objc): fold category/extension interfaces into the base class (#1556)#2501xiongjianxu wants to merge 1 commit into
Conversation
…aphify-Labs#1556) `@interface Foo (Cat)` in `Foo+Cat.h` declares members of an EXISTING class, but the class node was keyed off the `Foo+Cat` file stem — so `Foo.h` and `Foo+Cat.h` produced TWO nodes labelled `Foo`. Every `[Foo ...]` receiver then had two type-def candidates, tripped the member-call resolver's single-definition god-node guard, and emitted nothing: moving a method from a class body into a category silently destroyed call edges the same corpus resolved fine before. Category and class-extension interfaces (and implementations) now key off the base stem, and `_merge_decl_def_classes` folds a category header into the base header instead of bailing on "more than one header". Scoping: the fold is keyed on the CATEGORY SYNTAX, not on a `+` in the filename, so `Extra+Helpers.h` declaring a plain `@interface Helper` keeps its own stem. Two non-category headers still bail to id-disambiguation, and same-named classes in different directories stay distinct (the id embeds the directory path).
There was a problem hiding this comment.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
This PR addresses ObjC category and class-extension interfaces (@interface Foo (Cat) / @interface Foo ()) that were keying their class node off the full file stem (e.g. Foo+Cat), which minted a second node for the same class. It adds helpers to detect category/extension syntax and strip the +-suffix down to a base stem, uses them when generating class/impl node ids in the ObjC extractor, and relaxes the decl/def class merge logic to fold multiple sibling headers (base + category) into a single keeper header. A new test file exercises category method resolution, class-extension folding, and cases where the fold should not apply, alongside changelog entries.
No blocking issues surfaced. 5 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1474 functions depend on the 327 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
extract()— 370 callers, 39 callees - worse:
walk()— 1 callers, 10 callees
Verification — 1474 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 781 function(s) in the blast radius were not formally verified this run
· 2 more finding(s) on lines outside this diff (see the check run).
Problem
An Objective-C category (
@interface Foo (Cat)) or class extension (@interface Foo ()) declares members of an existing class. But the extractor keyed the class node off the file stem:So
Foo.handFoo+Cat.heach mint a node labelledFoo._merge_decl_def_classescannot fold them — it requires exactly one header per id-collision group, and category files reach it as two distinct headers.The consequence is not a cosmetic duplicate; it destroys edges. Controlled experiment, same call site, only the location of the method declaration differs:
-useItis declaredcallsedgesBase.h's@interface Base-go -> -useItEXTRACTED ✅Base+Extra.h's@interface Base (Extra)Measured on 0.9.34:
Categories are pervasive in real ObjC (
NSString+Trim.h, private extensions in every.m), so this hits ordinary code.grep -ic 'categor' graphify/extractors/objc.py→0: they were never handled.Fix
Two coordinated changes.
1.
graphify/extractors/objc.py— a category/extension interface (and implementation) keys its class node off the base stem:_objc_is_categorytests for the grammar's anonymous(child, which appears only for a category or extension — a generic class (@interface Box<T>) usesparameterized_arguments, so it is not matched. Verified against the tree-sitter-objc grammar:Being keyed on syntax rather than filename is what makes it safe:
Extra+Helpers.hdeclaring a plain@interface Helperkeeps its own stem._objc_category_base_stemadditionally splits only a well-formedName+Suffixpair, soC++Bridge.handFoo+.hare left intact.2.
graphify/extractors/resolution.py—_merge_decl_def_classesnow folds a category header into the base header. It already strips+when computing sibling stems (_decldef_class_stem); it just refused to pick a keeper when several headers were present. It now keeps the base header (the stem with no+), or the lowest-sorting category header when the base class lives outside the corpus (NSString+Trim.h,NSString+JSON.h). Two non-category headers still bail to id-disambiguation, so an unrelatedFoo.h/Foo.hpppair behaves exactly as before.Tests
New
tests/test_objc_category_interfaces.py— each fix case paired with a scoping case that must not change:test_objc_category_method_is_reachable_from_another_classBasenode;-go -> -useItEXTRACTEDtest_objc_class_extension_folds_into_the_base_class@interface Base ()inBase.mfolds;-pub -> -privtest_objc_non_category_interface_in_a_plus_named_file_is_untouchedExtra+Helpers.h's plain@interface Helperkeeps its own stemtest_objc_same_named_categories_in_different_directories_stay_distinctThingclasses ina/andb/stay separate;[Thing act]stays ambiguous → zero edgesThe two behavioral tests fail on
v8without the fix (verified by stashing both source changes: 2 failed, 2 passed).Verification
uv.lockuntouched. Base branch:v8.Notes
Issue creation is restricted for external contributors, so the reproduction is inline. This is a defect in the #1556 ObjC member-call pass, hence the reference.
Independent of my #2500 (protocol declarations as receiver types) — different files, no conflict — but the two compose: a corpus with both a category and a same-named protocol needs each fix to clear the god-node guard.