Skip to content

[Bug]: Swift: same-named nested types in one file collapse into one node (e.g. two CodingKeys enums) #4575

Open

Description

@makinggainz

Pre-flight checks

  • 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))
"

Error output or graph output

CodingKeys nodes: ['models_codingkeys']
[('CodingKeys', 'case_of', 'name'), ('CodingKeys', 'case_of', 'title'), ('CodingKeys', 'implements', 'CodingKey'), ('CodingKeys', 'implements', 'String'), ('Person', 'contains', 'CodingKeys'), ('Place', 'contains', 'CodingKeys')]

Graphify version

0.9.92

Operating System

macOS 26.5.2 (Apple silicon)

Python Version

3.12.13

Installation Method

uv tool install graphifyy

Additional Environment Details

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.

Activity

  1. github-actions commented on Oct 20, 2026

    @github-actions

    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.

  2. makinggainz commented on Oct 20, 2026

    @makinggainz
    Author

    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. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions