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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions