mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 05:59:11 +08:00
A TypeScript or JavaScript call on a constructed object, new C(args).m(),
reached the resolver as the bare method name in both engines, so exact
matching bound it to any project method called m: new RegExp(p).exec(s)
to a database wrapper's exec, new Array(n).fill(0) to a text class's
fill, new TextEncoder().encode(s) to a project encode function.
Both engines now emit new C().m (new ns.C().m for a namespaced class, and
nothing for a constructor that is not a plain name or member chain). The
resolver looks m up on C with resolveMethodOnType, inherited methods on
the conformance pass once extends edges exist, an aliased named import
mapped back to its class, and a JS built-in (RegExp, Map, Date...) never
a project class unless the file imports or declares one. An ES5
constructor function keeps its prototype method.
Our changes: the lookup passes the file the call's file imports the class
from (tsImportedTypeFile), so of two classes sharing a name the imported
one is the one constructed (the PR as written took the first indexed copy,
at 0.9); new this.constructor() and a static new this() keep the bare
method name, as a call on this does (the PR dropped three.js's clone
idiom, 29 links to a class's own copy); rebased onto PR #2852, which
edits the same keepsBareTsJsReceiver and test lists; resolved the import
list conflict with rustChainedCallByName from PR #2846; CHANGELOG bullet
moved next to the JS/TS receiver bullets.
Validation: the new tests fail 4 of 6 on main 449eefa7 with both engines
and pass here. A/B against main, kernel per arm: flask, gin and vite
byte-identical. codegraph 48 links added (new ToolHandler(cg).execute), 2
retargeted and 14 typed, all right, 58 removed, 57 wrong (RegExp exec and
test, Array fill, Set add) and 1 right one lost to the grammar's parse of
new nodeSqlite!.DatabaseSync(). excalidraw 22 wrong removed (TextEncoder,
TextDecoder, URLSearchParams, Array fill), 3 typed. pixijs 14 retargeted
and 278 typed, all right, 7 wrong removed. pdf.js 5 added, 47 retargeted
and 68 typed, all right, 76 wrong removed (Array keys and fill, typed
arrays, Response). three.js 33 added, 404 retargeted (Capsule::set to
Matrix4::set and Matrix3::set, NURBSCurve::copy to the class's own copy)
and 429 typed, all right; 135 removed, 133 wrong (UINumber onChange, which
three.js attaches at run time, TextDecoder, typed arrays, URL) and 2 right
only by name (Loader methods inherited through a base imported from the
three package). No default-imported class under another name lost an edge.
Kernel parity holds on all six TS/JS repos and this repository, 0 diffs.
Index CPU time unchanged or lower: codegraph 81.5 s to 79.5 s, three.js
72.0 s to 71.2 s (medians of 3, interleaved).
Original PR #1945 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
6.0 KiB
TypeScript
145 lines
6.0 KiB
TypeScript
/**
|
|
* A method called on an EXPRESSION receiver never binds by bare name to an
|
|
* unrelated project method. `(await list()).map(...)` in TypeScript and
|
|
* `v.iter().map(...)` in Rust both reached the resolver as the bare `map`,
|
|
* which exact-matched a TypeScript class's `map` — the Rust one across
|
|
* languages. TS/JS now looks through wrappers that keep the receiver
|
|
* (`(x)`, `x!`, `x as T`, `await`) and emits nothing for a receiver with no
|
|
* static type; the resolver refuses a bare-named call into another language
|
|
* family's class members.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { CodeGraph } from '../src';
|
|
import { extractFromSource } from '../src/extraction';
|
|
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
|
|
|
|
let dir: string;
|
|
let cg: CodeGraph;
|
|
|
|
const files: Record<string, string> = {
|
|
'src/adapter.ts':
|
|
'export class GraphAdapter { async map(req?: string) { return req ?? \'\'; } }\n' +
|
|
'export class Runner { run() { return 1; } }\n' +
|
|
'export class Base { hello() { return 1; } }\n',
|
|
// A project `push` an element of a field must not reach (`this.listeners[type].push`).
|
|
'src/stack.ts': 'export interface UndoStack { push: (entry: string) => void; }\n',
|
|
'src/bus.ts':
|
|
'export class Bus {\n' +
|
|
' private listeners: Record<string, string[]> = {};\n' +
|
|
' on(type: string, handler: string) { this.listeners[type].push(handler); }\n' +
|
|
'}\n',
|
|
'src/use.ts':
|
|
"import { GraphAdapter, Runner, Base } from './adapter';\n" +
|
|
"import { helper } from './helper.js';\n" +
|
|
'async function list(): Promise<string[]> { return []; }\n' +
|
|
'export async function names() {\n' +
|
|
' const a = (await list()).map((d) => d.length);\n' +
|
|
' const b = [1, 2].map((x) => x + 1);\n' +
|
|
' const c = (a ?? []).map((x) => x);\n' +
|
|
' return [a, b, c];\n' +
|
|
'}\n' +
|
|
'export function typed(g: GraphAdapter | undefined) {\n' +
|
|
' return g!.map();\n' +
|
|
'}\n' +
|
|
'export function fresh() { return new Runner().run() + helper(); }\n' +
|
|
'export class Child extends Base {\n' +
|
|
' greet() { return this.hello() + super.hello(); }\n' +
|
|
'}\n',
|
|
'src/helper.js': 'export function helper() { return 2; }\n',
|
|
'k/src/lib.rs':
|
|
'pub fn lens(v: Vec<String>) -> Vec<usize> { v.iter().map(|s| s.len()).collect() }\n' +
|
|
'pub fn opt(o: Option<u8>) -> Option<u16> { o.map(|x| x as u16) }\n',
|
|
// A cgo `//export` function called from Swift through the bridging header.
|
|
'export_ios.go': 'package main\n\nimport "C"\n\n//export OpenFluxStop\nfunc OpenFluxStop() {}\n',
|
|
'ios/Tunnel.swift': 'func stopTunnel() {\n OpenFluxStop()\n}\n',
|
|
// Kotlin's `Log.i(...)` reaches the resolver as the bare `i`.
|
|
'web/min.js': 'function i(a) { return a; }\n',
|
|
'android/Diag.kt': 'fun report() {\n android.util.Log.i("tag", "msg")\n}\n',
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
await initGrammars();
|
|
await loadAllGrammars();
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-exprrecv-'));
|
|
for (const [rel, text] of Object.entries(files)) {
|
|
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
|
|
fs.writeFileSync(path.join(dir, rel), text);
|
|
}
|
|
cg = CodeGraph.initSync(dir);
|
|
await cg.indexAll();
|
|
});
|
|
|
|
afterAll(() => {
|
|
cg.destroy();
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
const node = (name: string, file: string) =>
|
|
cg.getNodesByName(name).find((n) => n.filePath.endsWith(file) && (n.kind === 'function' || n.kind === 'method'))!;
|
|
const calleesOf = (name: string, file: string) =>
|
|
cg.getCallees(node(name, file).id)
|
|
.filter(({ edge }) => edge.kind === 'calls')
|
|
.map(({ node: n }) => n.qualifiedName)
|
|
.sort();
|
|
|
|
describe('expression receivers', () => {
|
|
it('TS: a call-result receiver does not bind `.map` to a project method', () => {
|
|
expect(calleesOf('names', 'use.ts')).toEqual(['list']);
|
|
expect(cg.getCallers(node('map', 'adapter.ts').id).map(({ node: n }) => n.name)).toEqual(['typed']);
|
|
});
|
|
|
|
it('TS: a method on an element of a field does not bind by its bare name', () => {
|
|
expect(calleesOf('on', 'bus.ts')).toEqual([]);
|
|
});
|
|
|
|
it('Rust: `v.iter().map()` does not bind to a TypeScript class method', () => {
|
|
expect(calleesOf('lens', 'lib.rs')).toEqual([]);
|
|
expect(calleesOf('opt', 'lib.rs')).toEqual([]);
|
|
});
|
|
|
|
it('keeps receiver-typed, bare, constructor, this/super and TS→JS calls', () => {
|
|
expect(calleesOf('typed', 'use.ts')).toEqual(['GraphAdapter::map']);
|
|
expect(calleesOf('fresh', 'use.ts')).toEqual(['Runner::run', 'helper']);
|
|
expect(calleesOf('greet', 'use.ts')).toEqual(['Base::hello']);
|
|
});
|
|
|
|
it('Kotlin: a bare-named call does not bind to a JavaScript function', () => {
|
|
expect(calleesOf('report', 'Diag.kt')).toEqual([]);
|
|
});
|
|
|
|
it('keeps a cross-language call to an exported free function (Swift → cgo)', () => {
|
|
expect(calleesOf('stopTunnel', 'Tunnel.swift')).toEqual(['OpenFluxStop']);
|
|
});
|
|
|
|
it('TS extraction: wrappers peel to the receiver, untyped expressions emit nothing', () => {
|
|
const src =
|
|
'async function f(x: X, y: any) {\n' +
|
|
' (await list()).map(g);\n' +
|
|
' x!.run();\n' +
|
|
' (y as X).run();\n' +
|
|
' (x satisfies X).stop();\n' +
|
|
' getTarget("a")!.install();\n' +
|
|
' (a ?? b).map(g);\n' +
|
|
' arr[0].run();\n' +
|
|
' f().list.map(g);\n' +
|
|
' (() => 1).call(null);\n' +
|
|
' this.a.b.run();\n' +
|
|
' this.listeners[type].push(h);\n' +
|
|
' this[k].stop();\n' +
|
|
" this['svc'].start();\n" +
|
|
' new Runner().go();\n' +
|
|
' window.Api.start();\n' +
|
|
'}\n';
|
|
const refs = extractFromSource('t.ts', src, 'typescript').unresolvedReferences
|
|
.filter((r) => r.referenceKind === 'calls')
|
|
.map((r) => r.referenceName);
|
|
expect(refs).toEqual([
|
|
'list().map', 'list', 'x.run', 'y.run', 'x.stop', 'getTarget().install', 'getTarget',
|
|
'f', 'run', 'this.listeners[].push', 'this.svc.start', 'new Runner().go', 'start',
|
|
]);
|
|
});
|
|
});
|