Skip to content

TS/JS: a member call whose receiver type lives in another repo is dropped, not parked (#3152) #3386

Description

@xiongjianxu

What happens

_resolve_typescript_member_calls binds this.greeter.greet() only when Greeter is
declared inside the same build. Split the two classes across two repositories and the
call disappears: the resolver has the receiver's type in hand, finds no declaration for
it, and drops the call without writing anything down. graph.json — the only artifact
merge-graphs and global add consume — records nothing, so no merge-time pass can
recover what was never written.

The merged two-repo graph is therefore missing exactly the edges that make it a call
graph. This is #3152 for TS/JS; Java, C++, C#, Swift and Objective-C already park.

Reproduction

app/src/app.ts
    import { Greeter } from "greeter-pkg";
    export class App {
        constructor(private greeter: Greeter) {}
        run(): void { this.greeter.greet(); }
    }

lib/src/greeter.ts
    export class Greeter {
        greet(): void {}
    }
$ graphify extract app  -o app/graphify-out
$ graphify extract lib  -o lib/graphify-out
$ graphify merge-graphs app/graphify-out/graph.json lib/graphify-out/graph.json --out merged.json

merged.json has no App.run -> Greeter.greet edge, and neither input graph.json
carries any trace of the attempt. Put both files in one build and the edge appears.

Two things had to be settled for TS/JS specifically

The uppercase-spelling arm must not park. The resolver types a receiver two ways: a
per-file table (constructor parameter properties, local new bindings, bare
type_identifier annotations) and "the receiver starts with a capital, so it is the
type". The second is a spelling heuristic that a namespace alias (import * as React),
a default import and a plain const object all satisfy. node_modules is excluded from
the scan (detect.py), so the overwhelming majority of receivers that resolve to no
local declaration through that arm are npm names — parking them would hand the merge a
pile of bare names to match with no evidence behind them. Only table-typed receivers
park.

One merge-side key for all eight suffixes. The merge pass checks that the declaring
file's suffix belongs to the parked language, which is what stops a Java Greeter from
answering through a Python class of the same name. TS and JS cannot be split here: a TS
class legitimately answers a JS call site, so two keys would drop every TS↔JS cross-repo
call. .ts .tsx .mts .cts .js .jsx .mjs .cjs share one key.

Also fixed on the way

The resolver was registered for .ts .tsx .mts .cts .js .jsx only, so a pure-ESM
(.mjs) or CommonJS-suffixed (.cjs) corpus never ran type-aware member-call
resolution at all — unrelated to merging, and visible in a single-repo build.

Relation to the origin gate (#2553)

The gate rejects a name-only match when the caller cannot see the matched type. It runs
after the single-definition check, so it only ever vets a name that did match locally.
The park happens at the zero-declarations point, strictly upstream of it, so parking can
never override a gate rejection. There is a test pinning that.

PR: sends the calls to the merge and adds the tests.

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