Skip to content

Swift imports edges are silently dropped from the final graph #1327

Description

@duncan-daydream

A report authored by Claude on missing 1st-party module-level imports in Swift AST parsing:

GraphifyModuleParsingIssue-repro.zip

Swift imports edges are silently dropped from the final graph

Summary

On a clean, from-scratch build, 100% of Swift import statements produce zero imports edges in
graphify-out/graph.json. The AST extractor does emit the edges, but it emits them pointing at a
bare module-name target node (e.g. corekit) that it never creates. build_from_json then drops
every edge whose target isn't a known node — treating first-party module imports as
"external/stdlib" — so all of them vanish.

This contradicts the documented behavior
(https://graphify.net/tree-sitter-ast-extraction.html: "import edges are module-level… extracted
without LLM"
). Other Swift relations (calls, implements, inherits, references, method,
contains) extract correctly — the gap is specifically module-level import resolution.

Environment

  • graphify 0.8.36 (installed via uv tool, Python 3.11)
  • tree-sitter-swift
  • macOS (Darwin 25.3.0)
  • Clean, from-scratch AST extraction (no graphify update, no prior curated state)

Minimal reproduction

A self-contained anonymized sample is attached. It is an Xcode app target plus a local SwiftPM
package with four cross-importing library modules, each exporting a public type named after its
module (mirrors a real monorepo).

GraphifyModuleParsingIssue/            # Xcode app target (imports FeatureAKit, FeatureBKit)
  GraphifyModuleParsingIssue/ContentView.swift
  GraphifyModuleParsingIssue/GraphifyModuleParsingIssueApp.swift
  Packages/
    Package.swift                      # 4 library products/targets
    Sources/CoreKit/CoreKit.swift
    Sources/NetworkingKit/NetworkingKit.swift   # import CoreKit
    Sources/FeatureAKit/FeatureAKit.swift       # import CoreKit, NetworkingKit
    Sources/FeatureBKit/FeatureBKit.swift       # import CoreKit, FeatureAKit

There are 7 internal import <Module> statements across these files (plus import SwiftUI and
import PackageDescription). A correct graph should contain ~7 corresponding imports edges.

Steps

  1. Build the graph from scratch (AST extraction; this is a code-only corpus).
  2. Run check.py (included) against graphify-out/graph.json.

Expected

~7 imports edges (e.g. FeatureAKit -> CoreKit, FeatureBKit -> FeatureAKit, etc.).

Actual

type=module nodes: 0 -> []
module dirs on disk: 4 -> ['CoreKit', 'FeatureAKit', 'FeatureBKit', 'NetworkingKit']
imports: 0 | references: 2 | inherits: 1 | implements: 1 | calls: 4
import edges:

Zero imports edges. Zero type=module nodes. Every other relation type extracted fine.

Root cause

The edges are produced by the extractor, then dropped at graph-build time.

1. The Swift import handler emits an edge to a module-name node it never creates.
_import_swift in graphify/extract.py (0.8.36):

def _import_swift(node, source, file_nid, stem, edges, str_path):
    for child in node.children:
        if child.type == "identifier":
            raw = _read_text(child, source)
            tgt_nid = _make_id(raw)        # e.g. "CoreKit" -> "corekit"
            edges.append({
                "source": file_nid,
                "target": tgt_nid,         # <-- no node with this id is ever appended
                "relation": "imports",
                ...
            })
            break

It appends an imports edge but never appends a corresponding node for the imported module.
(Contrast the JS/TS handlers, which resolve imports to real, existing file-path nodes.) So the
target id (corekit, featureakit, swiftui, …) is always dangling.

2. build_from_json drops every dangling-target edge as "external/stdlib".
graphify/build.py:

if src not in node_set or tgt not in node_set:
    continue  # skip edges to external/stdlib nodes - expected, not an error

Because every Swift import target is dangling, this prunes 100% of imports edges — including
first-party, in-repo module imports.

Evidence that the edges exist before the drop

Raw extract() output for this corpus contains 10 imports edges (all 7 internal + SwiftUI
×2 + PackageDescription), each with a target id that does not exist as a node:

featureakit_featureakit -> corekit        (target exists as node: False)
featureakit_featureakit -> networkingkit  (target exists as node: False)
featurebkit_featurebkit -> corekit        (target exists as node: False)
featurebkit_featurebkit -> featureakit    (target exists as node: False)
networkingkit_networkingkit -> corekit    (target exists as node: False)
...

After build_from_jsonto_json, the final graph has 0 imports edges.

This is not monorepo-specific

Running extraction on just the Packages/ SwiftPM package (graphify's documented happy path,
no app target) shows the same thing: raw extract() yields 6 imports edges, but after
build_from_json the final graph has 0. The defect applies to any Swift corpus.

Suggested fix

When the Swift import handler emits an imports edge, also emit a node for the imported module
(e.g. type=module, label = module name, id = _make_id(module_name)), the way other languages
anchor imports to a resolvable node. Optionally, parse Package.swift targets/products into
type=module nodes so first-party imports resolve to them while genuinely external modules
(SwiftUI, Foundation) can still be created-or-skipped deliberately rather than dropped by the
generic "dangling edge" pruning in build_from_json.

Attached

  • The full sample project (app + SwiftPM package).
  • check.py — the assertion script that produced the output above.
  • graphify-out/graph.json and graphify-out/GRAPH_REPORT.md from the from-scratch build.

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