Repository navigation
fix(swift): extract file-scope typealias declarations as type nodes - #4639
madhusudanpatnaik wants to merge 8 commits into
Conversation
The Swift extractor emitted no node for a `typealias`, so every field, parameter or return typed with one, even in the declaring file, resolved to a sourceless stub, and an `extension` of an aliased type had no declaration to merge onto. A file-scope alias now gets a type node with a `contains` edge from its file, minted with the id a top-level type gets, so same-file references bind to it, cross-file ones rewire to it and _merge_swift_extensions folds extensions onto it. Member aliases (associated-type witnesses such as `Element`), local aliases and same-name re-exports (`typealias Field = Core.Field`) get no node. tree-sitter-swift misparses many real files, and error recovery can lift a type's members to the top level or wrap its header in a bogus alias, so in a misparsed file an alias counts only when it parsed cleanly and the brace tokens before it balance. Bump the AST cache schema to 18: cached Swift results lack these nodes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the one-line _AST_CACHE_SCHEMA conflict: upstream is now at schema 20, so this branch's extraction change moves to 21, with upstream's history kept as "prior (20)". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the pull request, @madhusudanpatnaik. A maintainer will review it soon. Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions. A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic. |
There was a problem hiding this comment.
Graphify reviewed this change.
No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.
Not checked: tests were not run; formal verification proved 0 of 2 changed function(s) (2 not verified).
Formal verification. PR-changed functions: 0/2 verified (0 proven, 0 may-equivalent, 0 distinguished) · 2 not verified (2 unsupported).
Not verified on this run: \_extract\_generic (unsupported), \_swift\_extra\_walk (unsupported).
Graphify review — findings
Adds extraction of file-scope Swift typealias declarations as type nodes with a contains edge from the declaring file, so fields, parameters and returns typed with an alias resolve to a sourced node instead of a sourceless stub. Member aliases (in types, protocols, extensions), function-local aliases, and same-name re-exports like typealias Field = Core.Field still get no node. In files that tree-sitter-swift misparses, a top-level alias counts only if it parsed cleanly and the preceding {/} tokens balance, so error recovery can't promote a member alias to file scope. Bumps _AST_CACHE_SCHEMA to 21 so cached Swift extractions re-run.
No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3557 functions depend on the 463 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
extract()— 989 callers, 59 callees - worse:
_extract_generic()— 20 callers, 47 callees
Verification — 3557 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: 2171 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
73 of 437 test file(s) selected (17%) via static blast radius.
tests/test_astro_extraction.py— impacttests/test_build.py— impacttests/test_cache.py— impacttests/test_cache_stale_import_target.py— impacttests/test_charmap_encoding.py— impacttests/test_chunking.py— impacttests/test_cjs_module_extension.py— impacttests/test_cpp_nested_and_cli.py— impacttests/test_csharp_partial_classes.py— impacttests/test_definition_file_portability.py— impacttests/test_detect.py— impacttests/test_dotnet.py— impacttests/test_ejs_extractor.py— impacttests/test_ext_tagged_id_clash.py— impacttests/test_extract.py— impacttests/test_extract_cache_location.py— impacttests/test_extract_cli.py— impacttests/test_extract_php_closures.py— impacttests/test_grammar_cache_key.py— impacttests/test_ignore_file_encoding.py— impacttests/test_import_extension_resolution.py— impacttests/test_incremental_mtime_collision.py— impacttests/test_indirect_call_block_scoped_shadow.py— impacttests/test_indirect_dispatch.py— impacttests/test_indirect_dispatch_assign_return.py— impacttests/test_indirect_dispatch_getattr.py— impacttests/test_java_member_calls.py— impacttests/test_js_exported_scalar_bindings.py— impacttests/test_js_member_collisions.py— impacttests/test_language_override_lifecycle.py— impacttests/test_language_overrides.py— impacttests/test_languages.py— impacttests/test_llm_backends.py— impacttests/test_manifest_ingest.py— impacttests/test_multilang.py— impacttests/test_objc_field_table_remap.py— impacttests/test_out_dir_evidence.py— impacttests/test_partial_cache.py— impacttests/test_php_unparsed_casts.py— impacttests/test_pipeline.py— impacttests/test_python_inherited_method_calls.py— impacttests/test_python_property_reads.py— impacttests/test_python_pytest_fixture_receivers.py— impacttests/test_python_underscore_resolution.py— impacttests/test_rationale.py— impacttests/test_ruby_resolution.py— impacttests/test_rust_self_member_calls.py— impacttests/test_rust_use_trees.py— impacttests/test_scala_context_bounds.py— impacttests/test_scala_given_instances.py— impact- … and 23 more
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Docs that may be stale (advisory)
.github/PULL_REQUEST_TEMPLATE.md§ Graphify-specific checklist (lines 33-39): references changed symbolsrunBENCHMARKS.md§ Reproducing (lines 190-203): references changed symbolsrunCHANGELOG.md§ 0.9.90 (2026-10-13) (lines 71-89): references changed symbolsrunCHANGELOG.md§ 0.9.80 (2026-10-07) (lines 251-270): references changed symbolsrunCHANGELOG.md§ 0.9.68 (2026-09-25) (lines 431-443): references changed symbolsrunCHANGELOG.md§ 0.9.59 (2026-09-12) (lines 535-542): references changed symbolsrunCHANGELOG.md§ 0.9.53 (2026-08-30) (lines 611-622): references changed symbolsrunCHANGELOG.md§ 0.9.47 (2026-08-19) (lines 699-715): references changed symbolsrunCHANGELOG.md§ 0.9.45 (2026-08-16) (lines 733-739): references changed symbolsrunCHANGELOG.md§ 0.9.39 (2026-08-10) (lines 812-819): references changed symbolsrun
…and 10 more.
Formal verification
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: parameter `config` is annotated `LanguageConfig` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_swift\_extra\_walk.
The verifier did not have enough to check \_swift\_extra\_walk, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: signature changed: no shared positional arity
· 2 more finding(s) on lines outside this diff (see the check run).
The only conflict was the one-line _AST_CACHE_SCHEMA in graphify/cache.py: v8 is at 34, so this branch now bumps it to 35 and keeps v8's 34 entry in the history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d registry v8 now assigns file-stem declaration ids through DeclIdRegistry (Graphify-Labs#4854), so a declaration whose id spells its file's own id gets a salted id instead of collapsing into the file node. The typealias mint site this branch adds predates that and called add_node directly: `typealias Swift = Int` in Box.swift got no node of its own, and `var v: Swift` pointed at a stub. On SnapKit, an alias minted outside the registry also made the same-named `class ConstraintLayoutSupport` (the other `#if canImport(UIKit)` branch) and `extension View` salt themselves into duplicate nodes. Route it through decl_node_fn as the other file-stem declarations are, and read whether a node was minted off seen_ids after the call, so a salted alias still gets its `contains` edge. Register it as an "alias", as the C++ alias site does: as a "type" it would merge with a same-kind case twin, so `typealias string` and `struct String` would become one node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…parses v8's relock moved tree-sitter-swift from 0.7.2 to 0.7.4, which parses an `#if` inside a type body cleanly, so the two misparse fixtures stopped exercising the file-scope guard: the naive `parent == source_file` rule passed them. 0.7.4 still trips on a freestanding `#warning`/macro expansion in a type body and lifts the member alias after it to the top level; two fixtures of that shape fail under the naive rule again. The 0.7.2 fixtures stay, since pyproject allows that version. A canary test fails if no fixture misparses under the installed grammar any more, and the alias-after-a-misparse test runs on every fixture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test (3.13) failed on test_watch_config_changes_keep_semantic_work_visible[False] with FileNotFoundError on graph.json. That is the drive_watch harness race Graphify-Labs#4620 fixes: the harness patches time.sleep on the shared time module, so a sleep in subprocess's wait loop during the rebuild's `git rev-parse` raises the scripted KeyboardInterrupt and stops the watcher before it writes graph.json. 3.14 passed on the same commit. No code change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The only conflict was the one-line _AST_CACHE_SCHEMA in graphify/cache.py: v8 is at 36, so this branch now bumps it to 37 and keeps v8's 36 entry in the history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g in case With the exact-spelling lookup now in v8 (Graphify-Labs#4951), `typealias Session = Int` beside `func session()` resolves `var s: Session` to the alias. The alias is registered as an "alias", which the kind-ordered lookup used to try after "fn"; this pins that the exact spelling now decides. It fails on this branch's previous head, which predates the lookup change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What does this PR do?
The Swift extractor emitted no node for a
typealias. A field, parameter or return typed with one, even in the declaring file, therefore resolved to a sourceless stub. Also, anextensionof an aliased type had no declaration to merge onto, soextension ConstraintInsets(SnapKit) orextension KFCrossPlatformColor(Kingfisher) stayed a separate type node in whichever file extended it. On Kingfisher, all 24 references toKFCrossPlatformColorlanded on the extension node inImageProcessor.swift; they now land on the alias inKingfisher.swift, and the extension folds into it.Changes (
graphify/extractors/engine.py):A file-scope
typealias(typealias Handler = (Int) -> Void,public typealias JSON = [String: Any],typealias Pair<T> = (T, T), attributed or inside#if) becomes a type node contained by its file. It's handled in_swift_extra_walk, which returnsFalseso the walk goes on as before. The id is_make_id(stem, name), the oneensure_named_nodeand a top-level type use (Swift never populatesnamespace_stack). So same-file references bind directly, cross-file ones go through the existing rewire, and_merge_swift_extensionsfolds extensions onto the alias as it does for a struct.The alias node is minted through
decl_node_fn, the declaration-id registry v8 added for JS file/class ID collision drops class Js and emits self-contains edges (0.9.87) #4854, as every other file-stem declaration is. It's registered as analias, as the C++ alias site is. As atypeit would share an id with a same-kind declaration differing only in case, sotypealias string = [Character]andstruct String {}would become one node. Without the registry, an alias that takes its id outside it makes a later declaration of the same name salt itself: on SnapKit,#if canImport(UIKit)/typealias ConstraintLayoutSupport = UILayoutSupport/#else/class ConstraintLayoutSupport {}became the alias, a salted duplicate class node and a sourceless stub, andtypealias Viewbesideextension ViewinTests.swiftgave a salted duplicateView. Through the registry each is one node. An alias whose id spells its file's own id (typealias SwiftinBox.swift) also keeps a node of its own and itscontainsedge.Not declarations here, on purpose:
typealias Element = Intin a type, protocol or extension). They witness associated types that many types name (Element,Index,Iterator), and a node would capture references meant for those. My C++ alias PR (fix(cpp): extract namespace-scopeusingandtypedefaliases as type nodes #4580) skips member aliases for the same reason.typealias Field = Core.Field,Core.Field<Int>), which denote the original type. My Go and Python alias PRs (fix(go): extracttype A = Baliases as type nodes #4579, fix(python): extract explicit type aliases as type nodes #4578) use the same rule.Misparsed files. tree-sitter-swift fails to parse many real files cleanly. Error recovery can lift a type's members to the top level, or wrap a type's header inside a bogus
typealias_declaration. Which shapes misparse depends on the grammar version:#ifinside a type body is enough: 2 of 64 (Moya) up to 226 of 1,019 (RxSwift) files have errors.#warning("…")or macro expansion in a type body still lifts the member alias after it.pyproject.tomlallows both (>=0.7,<0.9).A first version that checked only
parent.type == "source_file"therefore minted member aliases as file-scope nodes. On RxSwift (0.7.2) one of them then claimed the case-folded id of a method (SectionIndexTitles/sectionIndexTitles()) and absorbed its edges. So_swift_file_scope_aliasesdecides scope once per file:{/}tokens before it balance. Comments and string contents are not brace tokens, and zero-width MISSING tokens are skipped.The helper looks only at the root's children and counts braces only in misparsed files that have top-level aliases. Over all 1,019 RxSwift files it takes 7 ms. Across the 6 repos below it rejects 9 lifted member aliases under 0.7.2 (RxSwift 4, swift-argument-parser 5) and none under 0.7.4. Under either grammar, every alias it keeps is file-scope.
graphify/cache.py: AST cache schema 34 → 35 (the branch includes v8 at 32e5efe). Cached Swift results lack these nodes.Type of change
Verification & Invariants
Invariant: a source-backed declaration wins over a sourceless stub, without making a previously correct resolution ambiguous, and without losing any edge.
Persisted state invalidated: AST cache schema bump, so the first build after upgrading re-extracts code files once.
Conflict note: my open variadic (fix(extract): variadic parameters reference their type (Go, Java, C#, Scala, PHP) #4638), Dart (fix(dart): name a legacy typedef that returns a function type by its own name #4641) and C++ alias-lookup PRs also bump the schema 34 → 35. Whichever merges later needs a one-line rebase. feat(swift): resolve type references across files through a Swift type index #4758 (Swift cross-file type index) also edits the start of the Swift block in
_extract_generic; whichever of the two lands second needs a small merge there, which I'll do.Related, not changed here: [Bug]: Swift: same-named nested types in one file collapse into one node (e.g. two
CodingKeysenums) #4575 (same-named nested Swift types collapse into one node) concerns nested type ids. This PR only adds file-scope ids, so it doesn't interact with a fix there.Read the CONTRIBUTING.md guide.
Reproduced the issue and identified the invariant.
Made the smallest fix necessary.
Added a regression test (if bug fix) or isolated boundary test.
Kept the PR description synchronized with the final implementation.
Documented any limitations / unsupported cases explicitly.
Limitations:
struct Foo {}+extension Foo: P {}in one file), the extension shares the declaration's id, so_merge_swift_extensionstreats the declaration as an extension. Cross-file extensions then don't fold onto it, and references elsewhere stay on a stub. It happens on v8 for a plain struct, and an alias behaves the same way: Kingfisher'sKFCrossPlatformImageis declared and extended inKingfisher.swift, so it doesn't benefit from this PR. That's a separate fix.typealias Session = Intandfunc session()in one file,var s: Sessionbinds to the function: the registry's lookup case-folds the name and triesfnbeforealias. The C++ alias site does exactly the same on v8 today. My C++ alias-lookup PR fixes the lookup for both: a reference binds to the declaration spelled exactly like it. With both PRs, the 6 Swift repos below give exactly the graph this PR gives alone.How was this tested?
New
tests/test_swift_typealias.py(18 tests):containsedges;#if/#elsepair as one node;#warningand a macro expansion that both misparse. Each is checked twice: its member alias gets no node, and a real alias after it does;typealias SwiftinBox.swift) keeping its own node;typealias string+struct String) staying two nodes.On v8 (32e5efe, grammar 0.7.4), 12 fail; the other 6 are 5 guards and the canary. Mutations, each run under both 0.7.4 and 0.7.2:
parent == source_filescope check fails the#warningand macro tests, plus the two#iftests under 0.7.2.has_errorcheck fails wrapped-header under 0.7.2. I found no shape that triggers it under 0.7.4.add_nodeinstead ofdecl_node_fnfails the file-extension test.typeinstead of analiasfails the case test.Real repos, full-graph diff v8 → this branch (v8 at 32e5efe, grammar 0.7.4). Nodes are counted as distinct ids; edges are compared per site (source label, relation, file, line), keeping multiplicity:
typealias(e.g. Kingfisher's 26 ontoKFCrossPlatformColorinKingfisher.swift: its 24 references, a call, and the extending file'scontains), because an extension node collapsed onto the alias it extends. That is also why node counts drop.private struct _Lockthat a misparse lifted. They are correctly left out.Determinism (re-checked on v8 32e5efe with this exact code): byte-identical graphs under
PYTHONHASHSEED=1and777, sequential and parallel extraction, and cold and warm caches (Kingfisher, SnapKit, Moya).Performance:
extract(), cold cache, 3 interleaved rounds, v8 (0.9.93) vs this branch: RxSwift 15.36/15.46/15.74 s vs 14.87/15.95/15.12 s, Kingfisher 2.91/2.87/3.42 s vs 2.85/2.99/3.04 s, SnapKit 0.40/0.42/0.46 s vs 0.41/0.42/0.57 s; peak RSS identical. Per file, no file is slower beyond noise (flagged files re-timed best-of-15: 0.99–1.03×).public typealias TestTime = Int; v8 reports them as yielding no symbols, and this branch extracts the alias._swift_type_parameters_in_scope, not here; my separate type-parameter scope-walk PR (perf(extract): make the Kotlin/Swift/Scala/C++ type-parameter scope walk linear) fixes it.Scenario checks:
PYTHONHASHSEED, parallel extraction, a warm cache, and a cache filled by v8 first.graphify updateover a graph v8 built equals a fresh build.updateagain equals a fresh build.Together with my other open PRs: merged onto v8 (32e5efe) with test(watch): script only the watch loop's sleep in drive_watch #4620, fix(extract): variadic parameters reference their type (Go, Java, C#, Scala, PHP) #4638, perf(extract): make the Kotlin/Swift/Scala/C++ type-parameter scope walk linear #4640, fix(dart): name a legacy typedef that returns a function type by its own name #4641 and my C++ alias-lookup PR, only the one-line
_AST_CACHE_SCHEMAincache.pyconflicts, and the full suite passes: 3.13: 9424 passed, only the 7 DNS-dependent tests failing.Graphify-specific checklist
uv run python -m tools.skillgen --bless) when changing their source fragments. (Ran--bless: all 134 artifacts regenerated with no changes, and--checkpasses. No source fragments are touched.)