Skip to content

fix(resolution): stop typed external receivers and nested member calls from fabricating method edges - #1651

Open
zyyyyynnn wants to merge 9 commits into
colbymchenry:mainfrom
zyyyyynnn:fix/1566-ts-builtin-receiver-resolution
Open

fix(resolution): stop typed external receivers and nested member calls from fabricating method edges#1651
zyyyyynnn wants to merge 9 commits into
colbymchenry:mainfrom
zyyyyynnn:fix/1566-ts-builtin-receiver-resolution

Conversation

@zyyyyynnn

@zyyyyynnn zyyyyynnn commented Aug 31, 2026

Copy link
Copy Markdown

Summary

Fixes #1566.

Prevents TypeScript/JavaScript calls on built-in and external receivers (such as Map.get, Map.set, Map.has, Promise.then, and external SDK imports) from incorrectly resolving to unrelated same-named project methods or fabricating self-edges, while preserving exact object-literal / store call chains (useStore.getState().reset(), get().reset()), strict lexical ownership verification on typed project instances, out-of-line method definitions (such as C++ class declarations in headers with method bodies in source files), and deterministic inheritance traversal.

Architecture

  1. Canonical Method Call Reference Parser (parseMethodCallReference):
    • Single authoritative parser across resolution strategies covering dot (a.b), C++ explicit operator (a.operator+), scope (A::b), Lua/Luau single colon (a:b), and R dollar (a$b).
  2. Visibility-Anchored Type Binding (bindProjectReceiverType):
    • Binds inferred receiver types strictly to visible project type nodes in the graph via same-file checks and resolved relative/project import mappings.
    • Ambiguous same-file same-named type declarations remain unresolved.
    • Built-in global types (Map, Set, Promise) and unresolved external imports (from "external-sdk") return null and never bind to project decoy classes.
  3. Structured Call-Result Chain Preservation & Container-Scoped Resolution (resolveTsJsCallResultMember):
    • Extractor preserves structured call-result chains (useStore.getState().reset, get().reset, factory().get) rather than degrading them to bare method names.
    • Dynamic and computed receivers with no static identity (holder[key].get()) remain unlinked.
    • Call-result chains resolve strictly against proven same-file or imported constant/variable containers; final member resolution uses the existing fix(resolution): resolve calls to object-literal namespace members (#1573) #1597 exact-container containment rules. Unproven call results remain safely unresolved with zero fallback to generic name guessing.
  4. Direct Lexical Method Ownership (getDirectCallableCandidatesOnTypeNode / resolveMethodOnTypeNode):
    • Direct typed method lookup is scoped by the bound type node's direct ownership identity (type::method); merely appearing inside the same file or textual class range is not sufficient evidence.
    • Exact member ownership is based on qualified type identity, not physical file co-location; same-file candidates are preferred, while cross-file exact definitions such as C++ out-of-line methods remain eligible.
  5. Node-Anchored Conformance Traversal (resolveTypedReceiverCallsViaConformance):
    • Conformance retry operates as a BFS along actual graph edges (implements, extends) originating from receiverTypeNodeId.
    • Inherited typed calls use the same direct-ownership rule at every visited supertype; textual containment alone is never treated as member ownership.
    • Same-depth candidates are collected independently per supertype, ensuring deterministic ambiguity detection across traversal orders.
  6. Direct Field & Constructor Ownership Verification (inferTsJsFieldReceiverType):
    • Resolves this.<field> from class properties, constructor parameter properties, and explicit initializers, verifying direct constructor ownership so nested classes within method bodies do not hijack outer class field types.

Safety Invariant

Receiver context is authoritative. A call edge is created only when the receiver binds to a specific project type or indexed container and the target member carries exact ownership evidence for that scope. Physical file co-location may help disambiguate candidates but is never required for ownership; same-name, source-range, or ambiguous candidates remain unresolved.

Regression Coverage

  • Map.get/set/has negative control: built-in Map method calls never fabricate edges to project methods (LRUCache).
  • Decoy Map isolation: unimported class Map extends BaseMap in another file does not capture built-in Map calls.
  • External SDK isolation: import { ExternalClient } from "external-sdk" never links to project ExternalClient or its base classes.
  • Direct method lexical ownership: typed Service receiver never resolves to nested Local::send or nested function save.
  • Inherited method lexical ownership: derived class call useDerived(service) never resolves to nested Local::run or nested function run within BaseService.
  • C++ out-of-line inherited method recall: Derived -> Base correctly resolves cross-file Base::run defined in .cpp when Base is declared in .hpp.
  • Same-depth ambiguity safety: multiple inherited targets at the same BFS depth decline resolution rather than arbitrarily picking the first match.
  • Same-file type disambiguation: local shadow Worker within function does not capture top-level Worker::run; deeper nested Worker inside inner function is not visible to outer scope.
  • Constructor parameter property isolation: nested Local constructor parameter property does not hijack Service constructor or fabricate Service.mailer.
  • Store & object-literal calls: useStore.getState().reset() and in-store get().reset() resolve to exact store actions while isolating top-level and sibling store decoys.
  • Declared factory safety: declared factory().reset() inside a store action resolves factory() without misconnecting to sibling store actions.
  • Same-name class disambiguation: distinct Engine classes in different packages resolve only to their own base hierarchy.
  • Incremental sync parity: incremental sync correctly resolves deferred typed calls across inherited supertypes.
  • WASM / Rust kernel extraction parity: 100% parity across all syntax shapes.

Related Issues

…s from fabricating method edges

When a receiver's local declaration or type annotation provides a concrete
type (such as built-in Map/Set, an external package, or a non-matching
class), failure to validate a project method on that type now stays
unresolved instead of falling through to unique method-name guessing.

In addition, TS/JS member-expression extraction now preserves static
receiver chains (e.g. \holder.values.get\, \	his.store.get\,
\	his.mailer.send\) in both the WASM extractor and the native Rust kernel.
Multi-segment TS/JS receivers enforce an exclusive precision boundary in
the resolver, preventing dropped-receiver calls from fabricating caller
edges or self-edges on same-named project methods.

Validated project receivers continue to resolve normally.

Fixes colbymchenry#1566
…project type nodes (colbymchenry#1566)

- Extract canonical method call reference parser (parseMethodCallReference) covering dot, C++ explicit operator, scope, Lua single colon, and R dollar syntax
- Bind inferred receiver types strictly to visible project type nodes in graph (bindProjectReceiverType) rather than loose type names
- Restructure typed receiver conformance retry as node-anchored BFS traversal following extends/implements graph edges
- Add constructor parameter property and direct ownership checks to avoid intermediate class hijacking of this.<field> receivers
- Add comprehensive positive, negative, same-name duplicate, and external SDK isolation regression tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TypeScript: built-in Map.get/set/has calls resolve to unrelated project methods

1 participant