mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 07:15:37 +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>
167 lines
7.0 KiB
TypeScript
167 lines
7.0 KiB
TypeScript
/**
|
|
* A method called on a `new C(...)` receiver resolves on `C` or nowhere.
|
|
* `new RegExp(p).exec(s)` used to reach the resolver as the bare `exec` and
|
|
* exact-match a project database wrapper's `exec`; the extractor now keeps the
|
|
* constructed class (`new RegExp().exec`), so a built-in or external class
|
|
* binds to nothing while a project class resolves its own (or inherited)
|
|
* method.
|
|
*/
|
|
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/db.ts':
|
|
'export class SqliteDatabase { exec(sql: string) { return sql; } get(k: string) { return k; } }\n' +
|
|
'export class Printer { toString() { return "p"; } }\n',
|
|
'src/runner.ts':
|
|
'export class Base { baseMethod() { return 1; } }\n' +
|
|
'export class Runner { run() { return 1; } }\n' +
|
|
'export class Sub extends Base { own() { return 2; } }\n',
|
|
'src/use.ts':
|
|
"import { Runner, Sub, Runner as R } from './runner';\n" +
|
|
"import * as rr from './runner';\n" +
|
|
'export function builtins(p: string, u: string, s: string) {\n' +
|
|
' const a = new RegExp(p).exec(s);\n' +
|
|
' const b = new URL(u).toString();\n' +
|
|
' const c = new Map([[1, 2]]).get(1);\n' +
|
|
' const d = new Date().getTime();\n' +
|
|
' return [a, b, c, d];\n' +
|
|
'}\n' +
|
|
'export function projectClasses() {\n' +
|
|
' return new Runner().run() + new Sub().baseMethod() + new Sub().own() + new R().run() + new rr.Runner().run();\n' +
|
|
'}\n',
|
|
'src/plain.js':
|
|
"const { Runner } = require('./runner');\n" +
|
|
'function jsCalls(p, s) {\n' +
|
|
' new RegExp(p).exec(s);\n' +
|
|
' new Set([1]).get(1);\n' +
|
|
' return new Runner().run();\n' +
|
|
'}\n' +
|
|
'module.exports = { jsCalls };\n',
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
await initGrammars();
|
|
await loadAllGrammars();
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-newrecv-'));
|
|
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('new-expression receivers', () => {
|
|
it('TS: a built-in or external constructed class binds to no project method', () => {
|
|
expect(calleesOf('builtins', 'use.ts')).toEqual([]);
|
|
expect(cg.getCallers(node('exec', 'db.ts').id)).toEqual([]);
|
|
expect(cg.getCallers(node('get', 'db.ts').id)).toEqual([]);
|
|
expect(cg.getCallers(node('toString', 'db.ts').id)).toEqual([]);
|
|
});
|
|
|
|
it('TS: a project class resolves its own and inherited methods, via alias and namespace too', () => {
|
|
expect([...new Set(calleesOf('projectClasses', 'use.ts'))]).toEqual(['Base::baseMethod', 'Runner::run', 'Sub::own']);
|
|
expect(cg.getCallers(node('run', 'runner.ts').id).map(({ node: n }) => n.name)).toContain('projectClasses');
|
|
});
|
|
|
|
it('JS: the same rules apply', () => {
|
|
expect(calleesOf('jsCalls', 'plain.js')).toEqual(['Runner::run']);
|
|
});
|
|
|
|
it('TS: of two classes that share a name, the one the file imports is the one constructed', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-newrecv-twin-'));
|
|
const twin: Record<string, string> = {
|
|
'src/a/runner.ts': 'export class Runner { run() { return 1; } }\n',
|
|
'src/b/runner.ts': 'export class Runner { run() { return 2; } }\n',
|
|
'src/aa/use-a.ts': "import { Runner } from '../a/runner';\nexport function useA() { return new Runner().run(); }\n",
|
|
'src/zz/use-b.ts': "import { Runner as Job } from '../b/runner';\nexport function useB() { return new Job().run(); }\n",
|
|
};
|
|
for (const [rel, text] of Object.entries(twin)) {
|
|
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
fs.writeFileSync(path.join(root, rel), text);
|
|
}
|
|
const graph = CodeGraph.initSync(root);
|
|
try {
|
|
await graph.indexAll();
|
|
const target = (caller: string) => {
|
|
const from = graph.getNodesByName(caller).find((n) => n.kind === 'function')!;
|
|
return graph.getCallees(from.id).filter(({ edge }) => edge.kind === 'calls').map(({ node: n }) => n.filePath);
|
|
};
|
|
expect(target('useA')).toEqual(['src/a/runner.ts']);
|
|
expect(target('useB')).toEqual(['src/b/runner.ts']);
|
|
} finally {
|
|
graph.destroy();
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("a class building its own instance, with `new this.constructor()` or a static `new this()`, calls its own method", async () => {
|
|
// three.js's `clone() { return new this.constructor().copy(this); }`: no class name is
|
|
// written, so the call keeps the bare name, as a call on `this` does.
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-newrecv-self-'));
|
|
const own: Record<string, string> = {
|
|
'src/box.js':
|
|
'export class Box {\n' +
|
|
' copy(other) { return this; }\n' +
|
|
' init() { return this; }\n' +
|
|
' clone() { return new this.constructor().copy(this); }\n' +
|
|
' static create() { return new this().init(); }\n' +
|
|
'}\n',
|
|
'src/other.js': 'export class Other {\n copy(o) { return o; }\n init() { return 1; }\n}\n',
|
|
};
|
|
for (const [rel, text] of Object.entries(own)) {
|
|
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
fs.writeFileSync(path.join(root, rel), text);
|
|
}
|
|
const graph = CodeGraph.initSync(root);
|
|
try {
|
|
await graph.indexAll();
|
|
const callees = (caller: string) => {
|
|
const from = graph.getNodesByName(caller).find((n) => n.kind === 'method')!;
|
|
return graph.getCallees(from.id).filter(({ edge }) => edge.kind === 'calls').map(({ node: n }) => n.qualifiedName);
|
|
};
|
|
expect(callees('clone')).toEqual(['Box::copy']);
|
|
expect(callees('create')).toEqual(['Box::init']);
|
|
} finally {
|
|
graph.destroy();
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('extraction keeps the constructed class in the ref name', () => {
|
|
const src =
|
|
'function f(p, s) {\n' +
|
|
' new RegExp(p).exec(s);\n' +
|
|
' (new ns.Widget<T>(1)).draw();\n' +
|
|
' new (pick())().go();\n' +
|
|
' new Runner().a.run();\n' +
|
|
'}\n';
|
|
const refs = extractFromSource('t.ts', src, 'typescript').unresolvedReferences
|
|
.filter((r) => r.referenceKind === 'calls')
|
|
.map((r) => r.referenceName);
|
|
expect(refs).toEqual(['new RegExp().exec', 'new ns.Widget().draw', 'pick']);
|
|
});
|
|
});
|