fix(js,ts): a call or new never lands on an interface or a type alias (#2801)

A call or `new` that a strategy resolved by name or through an import could
target a TypeScript interface or type alias, which nothing can call or make
an instance of. memfs's fs-core `result.ts` declares `interface Ok` beside
`function Ok` (and `Err` beside `Err`), and `Ok(...)` through an import
reached the interface. A `new` of a global that a project only declares a
type for (`new Response()`, `new Worker()`, `new Event()`) was linked by
name to that type, and jest's `write()` parameter call to a `type write`.

Root cause: no rule on what a call may target. Whichever strategy found the
name picked among same-named nodes by order, and nothing stopped it on a
type.

Fix (gateTargetKind): for the JS family, a `calls` or `instantiates` result
whose target is an `interface` or a `type_alias` moves to the value the
target's file declares under the same name (a function, class, component,
constant or variable, first by position): `function Ok` beside
`interface Ok`, a class merged with an interface. With no such value the
result is dropped, and the reference stays unresolved.

Validation:
- __tests__/call-value-targets.test.ts (3 tests): all 3 fail on main and
  pass on the kernel. Each of 5 rules removed in turn fails at least one.
- Sync equals a fresh index (CLI) when the value beside a type is added or
  removed, and when a project type for a global or a parameter appears.
- A/B on 14 TS/JS repos and 2 controls: memfs's 11 Ok/Err calls move
  from the interfaces to the functions; 165 `new` of globals dropped (vite
  27, nuxt 57, webdriverio 26, insomnia 47, jest 7, memfs 1) and jest's
  `write()` call on `type write`. Every dropped `new` names a global
  (Worker, Event, Response, Request, Headers, ReadableStream, Node's Module,
  mocha's Context/Suite) or a local variable. vben, outline, social-app,
  t3-turbo, nitro, vue3-antdv-admin, pathe, c12, flask and gin are
  byte-identical.
- 91 related test files pass (2,178 tests).
- Cost: fresh index within noise (median of 3, interleaved): vite 9.07 s
  to 8.74 s, nuxt 11.71 s to 10.93 s, insomnia 15.26 s to 15.11 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-22 13:49:07 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e8ddc4e2be
commit 5d81d292fa
3 changed files with 130 additions and 0 deletions
+31
View File
@@ -158,6 +158,9 @@ const GO_BUILT_INS = new Set([
/** What a Pascal typecast's name resolves to: `TFoo(x)` casts, it doesn't call (see createEdges). */
const PASCAL_CAST_TARGET_KINDS: ReadonlySet<string> = new Set(['class', 'struct', 'interface', 'enum', 'type_alias']);
/** The values a TypeScript type may share its name with in its file, which a call or `new` of the name means. */
const TYPE_COMPANION_VALUE_KINDS: ReadonlySet<string> = new Set(['function', 'class', 'component', 'constant', 'variable']);
const PASCAL_UNIT_PREFIXES = [
'System.', 'Winapi.', 'Vcl.', 'Fmx.', 'Data.', 'Datasnap.',
'Soap.', 'Xml.', 'Web.', 'REST.', 'FireDAC.', 'IBX.',
@@ -3707,6 +3710,21 @@ export class ReferenceResolver {
!ref.referenceName.includes('.') && !isGoBareName(ref, this.context)) return null;
}
// A TypeScript interface or type alias is no value: nothing calls one or
// makes an instance of it. A strategy that found one by name meant the
// value its file declares beside it (`interface Ok` and `function Ok` in a
// result module); with none there, the name is a value from elsewhere
// that the project only types (`new Response()`, `new Worker()`), and
// links nothing.
if ((ref.referenceKind === 'calls' || ref.referenceKind === 'instantiates') && ESM_IMPORT_LANGUAGES.has(ref.language)) {
const target = this.nodeById(result.targetNodeId);
if (target && (target.kind === 'interface' || target.kind === 'type_alias')) {
const value = this.sameNamedValueOfType(target);
if (!value) return null;
result = { ...result, targetNodeId: value.id };
}
}
// An `imports` reference names something importable — never a member that
// only exists inside a type, nor its own import statement. A module path
// that names no file (`./missing`, `stdio.h`, `pkg.mod`, a C# `using` of
@@ -3948,6 +3966,19 @@ export class ReferenceResolver {
}
/** The one supertype-kind node a TypeScript value shares its name and file with. */
/**
* The value a file declares beside a type of the same name — `export
* function Ok()` beside `export interface Ok`, a class merged with an
* interface — which a call or `new` written with the name reaches. The
* first by position; null when the file declares none.
*/
private sameNamedValueOfType(type: Node): Node | null {
return this.context
.getNodesInFile(type.filePath)
.filter((n) => n.name === type.name && TYPE_COMPANION_VALUE_KINDS.has(n.kind))
.sort((a, b) => a.startLine - b.startLine || a.startColumn - b.startColumn)[0] ?? null;
}
private sameNamedTypeOfValue(value: Node): Node | null {
if (value.kind !== 'constant' && value.kind !== 'variable') return null;
if (value.language !== 'typescript' && value.language !== 'tsx') return null;