You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: Swift: same-named nested types in one file collapse into one node (e.g. two CodingKeys enums) #4575
I have checked the Troubleshooting section in the README
What happened?
Two Swift types nested in different parents in the same file, with the same name, collapse into one node. The common case is Codable structs that each declare enum CodingKeys. The node ID is {file_stem}_{name} with no enclosing type, so both enums become models_codingkeys. That one node is then contains-ed by both structs and carries the cases of both enums.
Expected: one node per nested type, for example models_person_codingkeys and models_place_codingkeys.
Impact on a mid-sized SwiftUI app: 23 CodingKeys enums in one file became a single node with about 150 edges. It ranked #3 among the god nodes and was its own large, low-cohesion community.
Steps to reproduce
mkdir nested-repro &&cd nested-repro && git init -q
cat > Models.swift <<'EOF'struct Person: Codable { let name: String enum CodingKeys: String, CodingKey { case name = "full_name" }}struct Place: Codable { let title: String enum CodingKeys: String, CodingKey { case title = "place_title" }}EOF
graphify update .
python3 -c "import json; g = json.load(open('graphify-out/graph.json')); L = g.get('links', g.get('edges'))lab = {n['id']: n['label'] for n in g['nodes']}ck = [n['id'] for n in g['nodes'] if n['label'] == 'CodingKeys']; print('CodingKeys nodes:', ck)print(sorted((lab.get(e['source']), e['relation'], lab.get(e['target'])) for e in L if e['source'] in ck or e['target'] in ck))"
No provider API keys set. Reproduced in a clean directory with only the file above. tree-sitter 0.25.2, tree-sitter-swift 0.7.4.
Additional context
0.9.92 fixed similar same-file ID collisions for Python (#4467) and Go/Rust (#4468). Swift nested types seem to need the same treatment, with the enclosing type folded into the ID. It probably affects any nested type name that repeats across parents, not just CodingKeys (for example nested Kind, State or Style enums).
This report and its reproduction were prepared with the help of an AI assistant and checked by running the steps above.
The fix. For Swift, make a nested type's id _make_id(parent_class_nid, class_name), as Python nested classes already do (#4220). That alone fixes the merge, but two more pieces are needed so nothing else regresses.
1. Members share that id shape. Methods, computed properties and enum cases get _make_id(parent_class_nid, name), case-folded. So struct Crime next to static func crime() -> Crime collapses into one node, and the factory's call becomes a self-loop. This pattern is common in Swift (var header: Header). What worked:
keep the plain id for the member;
give the nested type a salt using the _underscore_salted_nid scheme (sha1 of the name, 6 characters);
count members across every body that shares the parent's id, so a same-file extension counts too.
2. Bare references to nested types. Today a nested type named bare inside its parent, such as the Key in func label(for key: Key), resolves to {stem}_key. With parent-qualified ids, ensure_named_node misses it and mints a stub. When the corpus has several nested types with that name, the stub never resolves. What worked:
while walking a type's body, push the type's id on a Swift-only scope stack;
in ensure_named_node, try _make_id(enclosing, name) first, from the innermost scope outward;
Swift allows the reference before the declaration, so take the nested ids from a pre-scan that mirrors the walk. A type counts as nested only when it sits directly in its parent's body or under an ERROR;
if the walk never mints a pre-scanned id, fall back to the plain lookup, so no edge can dangle.
Also bump _AST_CACHE_SCHEMA. Top-level ids don't change.
Result. On a mid-sized SwiftUI app, the single CodingKeys node (147 edges, #3 god node) became 63 nodes, the largest with 32 edges. Dangling edges and self-loops on its Swift files are the same as on 0.9.92.
Tests. These regression tests fail on 0.9.92 and pass with the change:
the two-CodingKeys repro;
bare references written before and after the nested declaration;
a same-file extension reaching the type's nested enum;
the Crime / crime() collision.
Two further tests check that top-level ids are unchanged and that ids are stable across a warm cache and a relocated checkout. The full suite shows no new failures.
I have this on a local branch and can open a PR if that would help.
Written with the help of an AI assistant; the steps and numbers above come from running the change.
Activity
github-actions commentedon Oct 20, 2026
Thanks for opening this issue, @makinggainz. A maintainer will take a look soon.
If you would like to discuss it in real time, come say hi on our Discord server. For longer-form questions and ideas there is also GitHub Discussions.
To help us triage, please make sure the report includes what you expected, what actually happened, and the steps (and a small sample) to reproduce it.
makinggainz commentedon Oct 20, 2026
I have a fix working locally.
The fix. For Swift, make a nested type's id
_make_id(parent_class_nid, class_name), as Python nested classes already do (#4220). That alone fixes the merge, but two more pieces are needed so nothing else regresses.1. Members share that id shape. Methods, computed properties and enum cases get
_make_id(parent_class_nid, name), case-folded. Sostruct Crimenext tostatic func crime() -> Crimecollapses into one node, and the factory's call becomes a self-loop. This pattern is common in Swift (var header: Header). What worked:_underscore_salted_nidscheme (sha1 of the name, 6 characters);extensioncounts too.2. Bare references to nested types. Today a nested type named bare inside its parent, such as the
Keyinfunc label(for key: Key), resolves to{stem}_key. With parent-qualified ids,ensure_named_nodemisses it and mints a stub. When the corpus has several nested types with that name, the stub never resolves. What worked:ensure_named_node, try_make_id(enclosing, name)first, from the innermost scope outward;ERROR;Also bump
_AST_CACHE_SCHEMA. Top-level ids don't change.Result. On a mid-sized SwiftUI app, the single
CodingKeysnode (147 edges, #3 god node) became 63 nodes, the largest with 32 edges. Dangling edges and self-loops on its Swift files are the same as on 0.9.92.Tests. These regression tests fail on 0.9.92 and pass with the change:
CodingKeysrepro;Crime/crime()collision.Two further tests check that top-level ids are unchanged and that ids are stable across a warm cache and a relocated checkout. The full suite shows no new failures.
I have this on a local branch and can open a PR if that would help.
Written with the help of an AI assistant; the steps and numbers above come from running the change.