mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 12:00:02 +08:00
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:
co-authored by
Claude Opus 5.5
parent
e8ddc4e2be
commit
5d81d292fa
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user