Skip to content

TypeScript: calls to methods of exported object-literal constants resolve no edges (same-file or cross-file) — constant is not a STATIC_MEMBER_CONTAINERS kind #1573

Description

@IAliceBobI

Summary

Methods defined on an exported object-literal constant are extracted as nodes, but no call site of the shape obj.method() ever links to them — not from another file via import, and not from the defining file itself. codegraph callers / impact return zero, and because this is a static property access (not one of the runtime-dispatch forms), boundary surfacing (#835) doesn't flag it either — the gap is fully silent.

Repro (v1.5.0, macOS arm64)

a.ts:

export const obj = { m() { return 1; } };
export class C { static s() { return 2; } }
export function sameFileCallers() { return obj.m() + C.s(); }

b.ts:

import { obj, C } from "./a";
export function crossFileCaller() { return obj.m() + C.s(); }

After codegraph init:

$ codegraph callers m
ℹ No callers found for "m"        # expected 2 (sameFileCallers, crossFileCaller)

$ codegraph callers s

Callers of "s" (2):
function    sameFileCallers
  a.ts:3

function    crossFileCaller
  b.ts:2

Identical call shapes in the same expressions; the only difference is the container kind.

Real-world scale

In a real TypeScript repo, one exported object-literal serves as the app's API-wrapper namespace (100+ methods, called from everywhere). Measured there: one method with 170+ call sites reports 0 callers, and the wrapper's core call method has ~100 call sites in the defining file alone with ≈0 resolved. export const api = {...} is a common way to organize an API surface in TS, so callers/impact systematically under-report on repos using it.

Root cause

src/resolution/import-resolver.ts:

const STATIC_MEMBER_CONTAINERS = new Set<Node['kind']>([
  'class', 'struct', 'union', 'interface', 'enum', 'trait', 'protocol',
]);

Container.member resolution (#825) only descends into those kinds. A constant container resolves to the constant node and stops — the member node is never reached, so the edge is never minted (the same-file path included).

Suggested direction

Extending the #825 mechanism to constant containers whose initializer is an object literal looks natural: members are already looked up by ${container.qualifiedName}::${member} scoped to the container's file, so name+file scoping should keep the same decoy-safety properties validated for chained resolution in #750. Happy to work on a PR if this direction sounds right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions