Skip to content

[Bug] AST extractor emits no cross-file call edges (observed on Swift; affects all non-Python languages) #348

Description

@hrp749492216

[Bug] AST extractor emits no cross-file call edges (observed on Swift; affects all non-Python languages)

Summary

graphify's AST extractor builds label_to_nid per-file (extract.py L845-849) and only emits a calls edge when the callee resolves within the caller's own file. Foo.bar() where Foo lives in another file silently produces no edge. In a 726-node Swift graph (SrutiFlow, 160+ files) this yields 0 cross-file calls edges out of 125 total. GRAPH_REPORT.md then flags widely-used utilities as "isolated nodes"/"knowledge gaps" — a false positive.

Expected

calls edges should connect caller to callee regardless of declaring file. Build a project-wide symbol table after per-file AST extraction and resolve call sites against it.

Actual

  • label_to_nid (L845-849) is built only from the current file's nodes.
  • tgt_nid = label_to_nid.get(callee_name.lower()) (L949) returns None for cross-file callees; no edge is appended.
  • _resolve_cross_file_imports (L2039-2170) is Python-only (L2652-2658) and handles only from .module import Name, not call sites.
  • Swift resolver (L861-872) pulls the navigation_suffix simple_identifier — the last component of Foo.bar(), "bar". The receiver is discarded, so a global table keyed on Type.method could not be queried as written.

Side bug: save_cached / save_semantic_cache (cache.py L63-77, L119-154) key AST and semantic passes with the same file_hash(path); semantic runs clobber AST cache. In SrutiFlow, NetworkConfiguration.swift therefore has no AST nodes at all in graph.json — only the LLM description node (source_location: null).

Reproduction

// util.swift
enum Util { static func run() {} }
// caller.swift
func go() { Util.run() }

Run graphifyy on the folder. Expected: calls edge go → Util.run. Actual: none; Util reported isolated.

Evidence from SrutiFlow

Graph degree from graph.json. Caller count = distinct non-declaring .swift files referencing the symbol (excluding comment-only mentions).

Symbol Declared at Graph degree Non-declaring callers Missing edges (examples)
NetworkConfiguration (enum) SrutiFlow/Services/Network/NetworkConfiguration.swift 0 (only semantic LLM node) 12 (11 prod + 1 test) {Google,Anthropic,OpenAICompatible}LLMService, LLMProviderFactory, {Deepgram,Gladia,OpenAICompatible,OpenRouter,File}STTService, AudioFileValidator, CertificatePinningDelegate, CloudLLMServiceTests
InputSanitizer (enum) SrutiFlow/Services/Security/InputSanitizer.swift 4 (all intra-file) 4 (2 prod + 2 test) LLMProcessingService, KeyboardViewModel, SecurityServicesTests (9 calls), IntegrationTests (2 calls)
ErrorRecovery (enum) SrutiFlow/Services/Security/ErrorRecovery.swift 3 (all intra-file) 1 (test only; the STT grep hit is a comment) SecurityServicesTests.recoverySuggestion × 8

Global check: of 125 calls edges, 0 span files (source_file of source ≠ target); 118 same-file, 7 with one endpoint missing metadata.

Root cause (confirmed in source)

graphify/extract.py:

  • L845-849: label_to_nid local to _extract_generic, rebuilt per file.
  • L949-950: edge appended only if callee resolves locally.
  • L861-872 (Swift): drops receiver of navigation_expression.
  • L972-977: valid_ids = seen_ids filter discards any cross-file edge.
  • L2652-2668 + _resolve_cross_file_imports: cross-file linking only for .py, only for imports.

graphify/cache.py L63-77, L119-154: AST and semantic share one hash-keyed cache entry; latter overwrites former.

Impact

  • "Isolated nodes" / "knowledge gaps" in GRAPH_REPORT.md are unreliable on non-Python codebases; foundational utilities are flagged as dead code despite being project-wide dependencies.
  • Community detection and centrality/betweenness run on a graph missing most real call edges; clusters and importance rankings are systematically skewed.
  • Users following graphify's dead-code signal risk deleting live code. Worth a README warning until fixed.

Suggested fix direction

Generalize _resolve_cross_file_imports into a language-agnostic second pass that also resolves call sites. For navigation-style syntax (Swift/Kotlin/Scala) preserve the receiver so callees can be looked up as Type.method. Give AST and semantic extractions distinct cache keys.

Environment

  • graphify (pypi graphifyy): 0.4.1
  • Python: 3.10.19
  • OS: macOS / Darwin 25.4.0
  • tree-sitter: 0.25.2, tree-sitter-swift: 0.0.1
  • Target codebase: Swift 6, iOS 17+

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