Skip to content

Swift: computed properties emit no node — erases the entire SwiftUI view layer (var body is a computed property) #2181

Closed

Description

@drewmerc302

Version: graphify 0.9.25
Language: Swift (_SWIFT_CONFIG, extract.py:984)

Summary

_SWIFT_CONFIG treats only function_declaration / init_declaration / deinit_declaration / subscript_declaration as functions. A Swift computed property parses as property_declaration → computed_property, which appears in none of function_types, function_boundary_types, or body_fallback_child_types.

Consequence: a computed property gets no node, and its contents are never walked as a body. In SwiftUI, var body: some View { … } is a computed property — so for a SwiftUI codebase this silently erases the entire view layer.

The graph still looks healthy afterwards (types, funcs, and imports are all present), which is what makes this hard to notice: query and explain return confident, well-formed results that simply omit the UI, with no signal that anything is missing.

Reproduction

struct PlayerScrubber: View {
    var body: some View {
        VStack { Slider(value: .constant(0), in: 0...1) }
        .contextMenu {
            Button { mode.toggle() } label: { Label("Scrub Chapter", systemImage: "book") }
        }
    }
    var toggled: Int { 1 }
    func realFunction() -> Int { 2 }
}

tree-sitter-swift parse (abridged):

class_declaration              L1  struct PlayerScrubber: View {
  class_body                   L1  {
    property_declaration       L2  var body: some View {
      computed_property        L2  {          <-- not a recognised body type
    property_declaration       L9  var toggled: Int { 1 }
      computed_property        L9  { 1 }      <-- same
    function_declaration       L10 func realFunction() -> Int { 2 }
      function_body            L10 { 2 }      <-- recognised

Resulting graph: nodes for PlayerScrubber and .realFunction(). Nothing for body or toggled.

Observed on a real ~6.8k-node / 17k-edge SwiftUI project: a 108-line view file yields exactly two nodes — the file and the struct. Every Slider, .contextMenu, and Button inside body is invisible. In that same file a static func on a sibling enum extracts correctly, so the omission is specifically computed properties, not the file or the type.

Practical symptom that led me here: explain "<Type>" works well, but no query can answer "where is this control wired into the UI?" — because the answer lives inside body and body does not exist in the graph.

Why the obvious fix is wrong

Adding property_declaration to function_types would also match stored properties (var x = 5), which are not functions and have no body. On a medium codebase that mints thousands of junk nodes and leaves the graph worse than before.

Suggested fix

Emit a function-like node only when a property_declaration has a computed_property child, and treat that computed_property as the body/scope boundary.

That condition is on tree shape, so it doesn't fit the declarative LanguageConfig — but there's existing precedent for exactly this shape in _swift_extra_walk, which already special-cases Swift enum cases.

Worth handling in the same pass, since both also parse as computed_property:

  • willSet / didSet observers on stored properties
  • explicit get { } set { } accessor blocks

Impact

Any SwiftUI codebase, and any Swift codebase that puts logic in computed properties. Swift support is otherwise strong here — extension merging, receiver-typed member-call resolution, and module-aware imports all work well, which is why this one gap is easy to miss.

Related

Distinct from #2147, but they compound on SwiftUI projects. On the same corpus, View (degree 210), Foundation (264), SwiftUI (111) and RhapsodeKit (275) act as universal connectors, so path "A" "B" between two symbols in the same feature returned a five-hop route through View, an unrelated screen type, and Bool. #2147 covers why those hubs exist; this issue covers why the real intermediate nodes — the view bodies that would otherwise carry the connection — are missing. Fixing one without the other leaves SwiftUI traversal weak either way.

Happy to send a PR

If you can confirm the fix belongs in _swift_extra_walk rather than somewhere else, I'm glad to implement it. I have a 6.8k-node SwiftUI project currently exhibiting the bug, so I can report before/after node and edge counts on a real corpus rather than only a synthetic fixture — useful given this will shift node counts for every Swift repo and presumably move some snapshot tests.

Activity

  1. safishamsi commented on Jul 27, 2026

    @safishamsi
    Member

    Fixed in v0.9.28 (#2220, thanks @ozdemirsarman). Swift computed and observed properties (var body: some View { ... }, get/set, willSet/didSet) now emit graph nodes, so SwiftUI views are no longer erased. https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.28

  2. drewmerc302 commented on Jul 27, 2026

    @drewmerc302
    Author

    Confirmed fixed on the original corpus. Before/after numbers as offered, on the real ~7k-node SwiftUI project rather than a fixture.

    Method: identical working tree and commit for both runs, extract --code-only --no-cluster --force so it's pure AST with no LLM or clustering in the loop. 455 code files, 409 Swift, ~56k Swift LOC.

    0.9.25 0.9.28 Δ
    nodes 6574 7147 +573 (+8.7%)
    edges 18005 18881 +876 (+4.9%)
    nodes removed — — 0

    All 573 new nodes come from .swift files, and none are orphans — each gets an inbound method edge from its owning type, plus 569 calls and 36 references outbound. So the body contents are being walked, not just the property node emitted. That was the part I was least sure would land.

    150 of the new nodes are .body. The previous graph had zero.

    The file I used in the report is a clean example. PlayerUI/PlayerScrubber.swift went 2 nodes → 3, and the new .body node has degree 8:

    PlayerScrubber -[method]-> .body
    .body -[calls]-> ScrubberMode
    .body -[calls]-> .effectiveMode()
    .body -[calls]-> .bounds()
    .body -[calls]-> .clock()
    .body -[calls]-> .wallClock()
    .body -[calls]-> .spoken()
    .body -[calls]-> .supportsChapterMode()
    

    That's the query I couldn't answer before — "where is this control wired into the UI" — now resolving to real project symbols instead of dead-ending.

    17 other files were in the same ≤2-node state and are now populated. Biggest single gain was Library/ItemDetailBody.swift, 19 → 45 nodes.

    Worth noting for anyone else measuring this: node counts shift by ~9% on Swift repos, so snapshot tests will move.

    Also confirming #2147 (v0.9.27) from the same corpus, since the two compound as expected: View, Foundation and SwiftUI are gone from the god-node ranking entirely. The top hubs are now all project types — RhapsodeKit (277), ActiveConnection (229), APIClient (198). Traversal between two symbols in the same feature no longer detours through View and an unrelated screen.

    Thanks @ozdemirsarman and @safishamsi — both fixes landed clean.

  3. ozdemirsarman commented on Jul 27, 2026

    @ozdemirsarman
    Contributor

    Confirmed fixed on the original corpus. Before/after numbers as offered, on the real ~7k-node SwiftUI project rather than a fixture.

    Method: identical working tree and commit for both runs, extract --code-only --no-cluster --force so it's pure AST with no LLM or clustering in the loop. 455 code files, 409 Swift, ~56k Swift LOC.

    0.9.25 0.9.28 Δ
    nodes 6574 7147 +573 (+8.7%)
    edges 18005 18881 +876 (+4.9%)
    nodes removed — — 0
    All 573 new nodes come from .swift files, and none are orphans — each gets an inbound method edge from its owning type, plus 569 calls and 36 references outbound. So the body contents are being walked, not just the property node emitted. That was the part I was least sure would land.

    150 of the new nodes are .body. The previous graph had zero.

    The file I used in the report is a clean example. PlayerUI/PlayerScrubber.swift went 2 nodes → 3, and the new .body node has degree 8:

    PlayerScrubber -[method]-> .body
    .body -[calls]-> ScrubberMode
    .body -[calls]-> .effectiveMode()
    .body -[calls]-> .bounds()
    .body -[calls]-> .clock()
    .body -[calls]-> .wallClock()
    .body -[calls]-> .spoken()
    .body -[calls]-> .supportsChapterMode()
    

    That's the query I couldn't answer before — "where is this control wired into the UI" — now resolving to real project symbols instead of dead-ending.

    17 other files were in the same ≤2-node state and are now populated. Biggest single gain was Library/ItemDetailBody.swift, 19 → 45 nodes.

    Worth noting for anyone else measuring this: node counts shift by ~9% on Swift repos, so snapshot tests will move.

    Also confirming #2147 (v0.9.27) from the same corpus, since the two compound as expected: View, Foundation and SwiftUI are gone from the god-node ranking entirely. The top hubs are now all project types — RhapsodeKit (277), ActiveConnection (229), APIClient (198). Traversal between two symbols in the same feature no longer detours through View and an unrelated screen.

    Thanks @ozdemirsarman and @safishamsi — both fixes landed clean.

    Thanks for taking the time to validate this so thoroughly on a real-world SwiftUI project and for sharing the before/after numbers. It’s great to see that the .body nodes are not only being emitted, but that their contents, calls, and references are now being traversed correctly.

    The improvements to the previously under-populated files and the god-node ranking are especially encouraging. The note about the ~9% node-count shift and its impact on snapshot tests is also very helpful.

    Really appreciate the detailed verification, Andrew!

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