Skip to content

Swift AST: cross-file IMPLEMENTS/REFERENCES edges are dropped (module-scope symbols need no import) #843

Description

@Smilewithoutfalling

Summary

On the AST track, symbols declared in a sibling file of the same Swift module cannot be resolved, so cross-file IMPLEMENTS and REFERENCES edges are dropped silently.

Swift imports are module-level, not file-level, and every file in one Swift target is one module — symbols declared in one file are visible in its siblings with no import statement at all. The AST resolver, however, resolves a relation target only through (a) a symbol declared in the same file, or (b) an imported binding:

  • src/wiki-engine/code-knowledge/ast/index.ts — IMPLEMENTS sites: localInterfaces.find(…), then bindings.localToFile.get(ifaceName), otherwise continue (it never reaches a third fallback).
  • src/wiki-engine/code-knowledge/ast/call-resolver.ts — resolveOneCall() for a bare callee: symbolsByFile.get(site.fromFile) first, then bindings.localToSymbolId / bindings.localToFile, otherwise returns the site unresolved.

That model is correct for TypeScript / TSX / Python / Go, where reaching a symbol in another file always requires an import or a relative path. It does not hold for Swift.

Minimal reproduction

Sources/P1/A.swift

import Foundation

public protocol LocalProto {
    func ping() -> String
}

public func helper() -> Int {
    return 42
}

Sources/P1/B.swift

import Foundation

public struct S: LocalProto {
    public func ping() -> String {
        return "s"
    }
}

public func caller() -> Int {
    return helper()
}

teamai codebase --extract <pkg> --project p1:

[AST: ast: 6 symbols, 3 imports (0 resolved), 2 calls (0 resolved), 0 edges]
[extract] p1 complete
  Files: 3
  Facts: 9 (relation:3, interface:1, component:5)
  Graph: 10 nodes, 4 edges

Neither struct S: LocalProto nor caller() → helper() yields a relation:

  • facts-cache.json holds component facts for Sources/P1/B.swift:3 and Sources/P1/B.swift:9, but no IMPLEMENTS relation for line 3 and nothing resolving the call at line 9.
  • gaps/detected.md reports high-orphan-ratio | 6/6 nodes have no graph connections, dependencies may not be fully extracted.
  • The 4 graph edges are all CONTAINS edges generated by the heuristic track; every code-ast edge is absent.

The missing dimension is specifically "another file in the same module"

A fixture that declares one conformance per shape — protocol Refined: LocalProto, struct S: LocalProto, enum E: LocalProto, actor A: LocalProto, class C: LocalProto, extension ExtSite: LocalProto — resolves 8/8 IMPLEMENTS facts when all declarations live in one file. So struct, enum, actor, class, a protocol refinement and an extension conformance are all captured; the capture is not class-specific and is not the problem. Splitting the declaration and the conformance across two files drops it entirely.

Impact

Any Swift package with more than one source file that declares a conformance or calls across files — i.e. practically every real package. Because the edges are simply absent rather than recorded as a gap, a consumer cannot distinguish "there is no dependency" from "this was not resolved".

Relationship to #842

Not introduced by that PR: before it, .swift files were not collected on either track, so the resolver never received a Swift specifier. Registering Swift is what exposes the limitation — which is also why that PR registers Swift straight into the EXTERNAL_IMPORT gap path for module imports rather than attempting module resolution.

Possible direction

Swift Package Manager places each target's sources under Sources/<Target>/ (tests under Tests/<Target>/), and all files in one target form one module.

  1. Derive the module root for a .swift file by walking up to the first directory under Sources/ or Tests/.
  2. When resolving a call site or an IMPLEMENTS target from a .swift file, fall back to a symbol declared anywhere inside that module root before giving up.
  3. Handle ambiguity explicitly — two targets in one package can declare the same symbol name. Pick deterministically or record a gap rather than emitting an arbitrary edge.
  4. Gate the fallback on the importer being Swift so the TS / Go / Python semantics are untouched.

Adjacent but distinct from the above: import MyLib is unresolvable today for the same underlying reason (the resolver has no directory listing), which is why #842 reports it as an EXTERNAL_IMPORT gap.

Activity

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