Skip to content

Swift: no client-side Route or HTTP_CALLS emitted, so cross-repo-intelligence returns 0 edges (URLSession is already in the pattern table) #1892

Closed

Description

@CaptainMittens

Version

dev build from source at 4da8f877 (tag v0.10.8 lineage). I am 19 commits behind origin/main; none of those 19 touch internal/cbm/service_patterns.c, src/pipeline/pass_cross_repo.c, or any Swift front-end file, and my own local commits do not touch them either. So this should reproduce on stock main.

Platform

macOS (Apple Silicon) — macOS 26.6.2, arm64

Install channel

Built from source

Binary variant

standard

What happened, and what did you expect?

A Swift client that calls an HTTP endpoint produces no HTTP_CALLS edge and no client-side Route node, so mode=cross-repo-intelligence can never link it to the server that serves that path. A TypeScript client making the byte-identical call links correctly.

Swift is not missing from the pattern table. internal/cbm/service_patterns.c lines 109-112 already carry:

    /* Swift */
    {"Alamofire", CBM_SVC_HTTP, NULL},
    {"Moya",      CBM_SVC_HTTP, NULL},
    {"URLSession", CBM_SVC_HTTP, NULL},

The gap is one step earlier: the Swift front end never emits the client-side Route node + HTTP_CALLS edge carrying a url_path that the cross-repo matcher joins on.

I expected the Swift client to behave like the TypeScript one — an HTTP_CALLS edge with url_path=/api/v1/widgets, and a CROSS_HTTP_CALLS edge naming the Go handler.

Note on #523, which asks "what is the minimum a call and a route need to share to be linked?" — the TypeScript half of this repro answers that, and shows the matcher itself is working. This report is the opposite failure: the matcher is fine, one language front end never feeds it. Not a duplicate of #523, #678, #706 (those are matcher/extractor bugs where the call side does produce an edge), nor of #551 (SwiftPM manifest resolution for package-graph edges, a different pass).

Reproduction

Three tiny repos, all declaring the same path /api/v1/widgets. The only variable is the client language. Script attached below; it writes the fixtures and prints the five MCP calls to make.

Results:

Repo Language Route nodes HTTP_CALLS CROSS_HTTP_CALLS to server
srv-go Go + chi 2 — —
cli-ts TypeScript + axios 2 2 2
cli-swift Swift + URLSession 0 0 0

TypeScript, working — MATCH ()-[r:CROSS_HTTP_CALLS]->() RETURN ...:

url_path          target_project   target_file   target_function   transport
/api/v1/widgets   xtest-srv-go     main.go       listWidgets       GET
/api/v1/widgets   xtest-srv-go     main.go       createWidget      POST

Swift, same path: cross_http_calls: 0, total_cross_edges: 0.

Graph schema for the Swift fixture shows no Route label and no HTTP_CALLS edge type. Its only edge for the URLSession call is a bare USAGE — no CALLS edge is emitted at all.

repro-swift-http.sh
#!/usr/bin/env bash
# Reproduce: a Swift client produces no HTTP_CALLS, so cross-repo-intelligence
# finds no edges. A TypeScript client with the identical path produces both.
# Builds three tiny repos, then index each and run cross-repo-intelligence.
set -euo pipefail
B="${1:-/tmp/xrepo}"
rm -rf "$B"; mkdir -p "$B"/srv-go "$B"/cli-ts "$B"/cli-swift

cat > "$B/srv-go/go.mod" <<'EOF'
module example.com/srv
go 1.22
EOF
cat > "$B/srv-go/main.go" <<'EOF'
package main

import "github.com/go-chi/chi/v5"

func main() {
	r := chi.NewRouter()
	r.Get("/api/v1/widgets", listWidgets)
	r.Post("/api/v1/widgets", createWidget)
	http.ListenAndServe(":8080", r)
}

func listWidgets(w http.ResponseWriter, r *http.Request)  {}
func createWidget(w http.ResponseWriter, r *http.Request) {}
EOF

cat > "$B/cli-ts/package.json" <<'EOF'
{ "name": "cli-ts", "version": "1.0.0", "dependencies": { "axios": "^1.6.0" } }
EOF
cat > "$B/cli-ts/client.ts" <<'EOF'
import axios from "axios";

export async function listWidgets() {
  return axios.get("/api/v1/widgets");
}

export async function createWidget(body: unknown) {
  return axios.post("/api/v1/widgets", body);
}
EOF

cat > "$B/cli-swift/Package.swift" <<'EOF'
// swift-tools-version:5.9
import PackageDescription

let package = Package(
    name: "CliSwift",
    targets: [.target(name: "CliSwift", path: "Sources/CliSwift")]
)
EOF
mkdir -p "$B/cli-swift/Sources/CliSwift"
cat > "$B/cli-swift/Sources/CliSwift/Client.swift" <<'EOF'
import Foundation

final class Client {
    func listWidgets() async throws -> Data {
        let url = URL(string: "https://example.com/api/v1/widgets")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }

    func createWidget(body: Data) async throws -> Data {
        var request = URLRequest(url: URL(string: "https://example.com/api/v1/widgets")!)
        request.httpMethod = "POST"
        let (data, _) = try await URLSession.shared.upload(for: request, from: body)
        return data
    }
}
EOF

for d in srv-go cli-ts cli-swift; do
  (cd "$B/$d" && git init -q . && git add -A \
    && git -c user.email=t@t -c user.name=t commit -qm init)
done
echo "Fixtures built in $B"
echo
echo "Now, through the MCP server:"
echo "  index_repository  repo_path=$B/srv-go     name=xtest-srv-go     mode=full"
echo "  index_repository  repo_path=$B/cli-ts     name=xtest-cli-ts     mode=full"
echo "  index_repository  repo_path=$B/cli-swift  name=xtest-cli-swift  mode=full"
echo "  index_repository  repo_path=$B/cli-ts     name=xtest-cli-ts     mode=cross-repo-intelligence  target_projects=[\"xtest-srv-go\"]"
echo "  index_repository  repo_path=$B/cli-swift  name=xtest-cli-swift  mode=cross-repo-intelligence  target_projects=[\"xtest-srv-go\"]"

On a real project

Same shape at scale, on a 462-file / 64k-line Swift app talking to a Go backend:

Go + TS backend repo Swift client repo
Route nodes 332 label absent
HTTP_CALLS edges 215 edge type absent
Method nodes 2,877 3,766
CALLS edges 20,650 12,017

Swift parsing itself is healthy — 3,766 methods, 12,017 call edges. Only the HTTP layer is missing. On the backend side the 215 HTTP_CALLS include its TypeScript frontend (frontend/lib/api/classes/items.ts alone contributes 20), so TS client extraction works in a real tree too.

In the real Swift repo the failure takes a slightly different route to the same place. A CALLS edge for the URLSession call does exist, but resolves onto a local property:

callee:     URLSession.shared.data
strategy:   suffix_match    confidence: 0.21
resolved:   <project>.HomeboxKit.Sources.HomeboxUI.PickedFile.data
file:       HomeboxKit/Sources/HomeboxFeatures/Chores/ChoresDetailViewModel.swift:105

Because cbm_service_pattern_* matches the library identifier in the resolved qualified name, and the resolved name is a local PickedFile.data, the check cannot fire. The Swift graph also has no Package label at all, so no framework identifier can ever appear in a resolved name — the Go/TS project has 215 Package nodes with an external flag.

That suffix_match at 0.21 may be worth a look on its own: Swift calls into system frameworks are being resolved onto same-named local members, which affects more than the HTTP case.

Project scale (if relevant)

Repro fixtures: 6 files. Real project: 462 Swift files / 64,101 lines against a 1,485-file Go+TS backend.

Activity

  1. CaptainMittens commented on Aug 29, 2026

    @CaptainMittens
    ContributorAuthor

    Follow-up on the two gaps #1896 deliberately left open. Both change what gets
    extracted, which CONTRIBUTING puts behind prior design discussion, so I am
    asking before opening a PR rather than after.

    1. A URL built by a constructor — code written and ready, not submitted

    #1896 makes a bare string argument reachable. Real Swift almost never writes
    one, because Swift has no URL literal. It writes this instead:

    URLSession.shared.dataTask(with: URL(string: "https://example.com/api/v1/widgets")!)

    The literal then sits two levels below the argument list. From the vendored
    grammar, dumped rather than guessed:

    value_argument
      name:  value_argument_label(simple_identifier)      "with"
      value: postfix_expression
               target:    call_expression
                            (simple_identifier)            "URL"
                            call_suffix
                              value_arguments
                                value_argument
                                  name:  value_argument_label   "string"
                                  value: line_string_literal
               operation: (bang)                           "!"
    

    extract_url_or_topic_arg sees only the outer node and gives up, so the URL
    never reaches the service-pattern table and no Route node forms.

    I have written and tested the fix, on
    fix/swift-nested-url-constructor,
    stacked on #1896. I have not opened a PR for it. It steps past the
    postfix_expression and into the constructor's own argument list, and unwraps
    only URL, URLComponents and URLRequest, so any other constructor keeps its
    own meaning. A non-literal argument such as URL(string: base + path) falls
    through to the ordinary handling unchanged.

    Reproduce-first, on extraction and pipeline: the two behaviour tests fail
    without it, the guard test (a non-URL constructor must not have its string
    borrowed by the outer call) passes either way. With it, 569 passed, 0 failed,
    and a Route node forms for /api/v1/widgets from the nested shape.

    Happy to open it, drop it, or rework it — your call. If the answer is that a
    nested-constructor unwrap does not belong in the extractor at all, that is a
    useful answer too and I will close the branch.

    2. Alamofire's AF. shorthand — I do not think this should be fixed as-is

    AF.request(…) is Alamofire's documented everyday form, and it does not match
    the pattern table. The table holds Alamofire, and AF.request contains no
    such text.

    The obvious fix is to add "AF." to http_libraries. I think that would be a
    mistake, because match_qn is an unanchored strstr:

    if (strstr(qn, patterns[i].library_id) != NULL) {

    A two-character token then also matches LEAF.request, PDF.parse,
    CSAF.get — anything ending in AF before a dot. I checked the three repos I
    have indexed and found no such identifier, so the risk is real but unmeasured
    rather than demonstrated.

    Making it safe means anchoring the match on a segment boundary, and that changes
    behaviour for every pattern in every language — a much bigger change than this
    issue, and one I would not make without you asking for it. Flagging it here so
    it is on the record rather than proposing it.

  2. DeusData commented on Sep 3, 2026

    @DeusData
    Owner

    Fixed on main by 5802ccdd (PR #1896, CaptainMittens): Swift calls now reach their argument list through call_suffix, so URLSession calls yield their URL string and the HTTP_CALLS edges are emitted. Thank you for the report — the reproduction is what made the root cause quick to confirm.

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

    parsing/qualityGraph extraction bugs, false positives, missing edgesux/behaviorDisplay bugs, docs, adoption UX

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions