Skip to content

fix(resolution): resolve calls to object-literal namespace members (#1573) - #1597

Open
colbymchenry wants to merge 1 commit into
mainfrom
fix/1573-object-literal-members
Open

fix(resolution): resolve calls to object-literal namespace members (#1573)#1597
colbymchenry wants to merge 1 commit into
mainfrom
fix/1573-object-literal-members

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Fixes #1573. Thanks @IAliceBobI — the report had the root cause exactly right, and the fix sits one layer up from the suggested spot (resolution rather than the container-kind set), for the reason below.

What was wrong

Methods of an exported object-literal constant — export const api = { call() {…}, get: () => {…} } used as a module's API surface — never received a call edge from api.call(), same-file or through an import. The members are extracted as plain functions with bare qualified names (call, not api::call) sitting inside the constant's source extent, so:

Net effect: callers / impact reported zero for methods called from everywhere, with no boundary warning because nothing about obj.method() looks dynamic.

What this does

Adds one helper that resolves a member by containment — a node named member whose source range lies inside the value's range, in the value's own file — and uses it from both halves:

Precision rules, all tested: calls accept callable kinds only; a declaration nested inside another member's body is not a member; nothing outside the value's range can donate a match — a same-named top-level function, or a method returned by a factory the value merely holds — so those cases keep today's behavior rather than guessing. Class statics (C.s()) and non-literal values are untouched.

Extraction and qualified names are deliberately left alone: changing how literal members are named would have to be mirrored in the native kernel byte-for-byte, and the resolver-side lookup is contained and language-gated.

Tests

  • The issue's repro end-to-end: sameFileCallers and crossFileCaller are both callers of m; a decoy m in a third file gets none; the C.s() static control resolves exactly as before; crossFileCaller no longer has a calls edge to the constant.
  • Arrow-property and method members both resolve; a function call() nested inside get's body is never taken for api.call().
  • A value holding a factory's result (const obj = makeObj()) with a same-named top-level m in the file: no false attribution, existing behavior kept.
  • The two positive tests fail on main; the control passes both ways, as a guard should.
  • Full suite: 189 files, 3181 passed / 9 skipped.

With the built CLI on the issue's a.ts/b.ts: codegraph callers m → 2 callers (sameFileCallers, crossFileCaller); callers s unchanged; edges sameFileCallers -> m (0.85) and crossFileCaller -> m (import, 0.9), none to obj.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK

…1573)

`export const api = { call() {…}, get: () => {…} }` used as a module's
API surface is a common TypeScript shape, and no call of the form
`api.call()` ever linked to the member: the members are extracted as
plain functions with BARE qualified names inside the constant's extent
(there is no `api::call`), so the `Container::member` lookup the
class-shaped kinds use (#825) bails on kind `constant`, the declared-type
inference for singleton instances (#1292) finds no type in a literal, and
the same-file strategies only consider classes and `method` kinds. The
result was an edge to the constant through an import and no edge at all
in the defining file — `codegraph callers` / impact reported zero for
methods called from everywhere.

Resolve the member by CONTAINMENT instead: a node named `member` whose
source range lies inside the value's range, in the value's own file.
One helper, used from both halves:

- import path: when the imported value is a constant/variable, try the
  literal member before the #1292 instance inference;
- same-file path: a same-file constant/variable receiver (TS/JS family
  only) is checked before the class-name strategies.

Precision rules: calls accept callable kinds only; a declaration nested
inside another member's body is not a member; nothing outside the
value's range can donate a match (a same-named top-level function, a
method returned by a factory the value merely holds). Class statics and
non-literal values keep their existing paths byte-for-byte.

Tests: the issue's repro (same-file + cross-file callers of `m`, a decoy
`m` in a third file untouched, the `C.s()` static control unchanged, no
edge to the constant), arrow + method members with a nested declaration
skipped, and a non-literal value receiver left on its existing path.
The two positive tests fail on main; the control passes both ways.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant