`serialize(this.raw)` inside `Record.serialize`, with a module-scope `function serialize` in the same file, resolved onto the method itself: both were exact-name candidates, both same-file, and findBestMatch's line-proximity term always prefers the enclosing method (#1714). In JS/TS a call written without a receiver cannot reach a method at all — methods need `this.`, an object, or a bound reference. The extractor emits `this.m()` and `super.m()` under the bare method name, so the receiver is read back from the call site's own line: when the text there begins with the name itself and nothing but whitespace, an operator or an opener precedes it, the call is bare, and `method` nodes leave the candidate set before ranking. matchFuzzy declines a lone `method` survivor for the same ref. A name the file binds itself also has no cross-file candidate for a bare call. `this.serialize()` and `other.serialize()` are unchanged. Rebased #1735 onto current main (resolved conflicts with sealed-module / cross-file visibility guards from #1719/#1730/#1731). Fixes #1714 Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com> Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
149 lines
5.5 KiB
TypeScript
149 lines
5.5 KiB
TypeScript
/**
|
|
* In JS/TS a receiver-less call can never bind to a class method: `serialize(x)`
|
|
* inside `Record.serialize` means the module-scope function, and the method
|
|
* itself — which the same-file proximity term used to pick, producing a
|
|
* self-edge — is not a candidate (#1714). `this.serialize(x)` still is.
|
|
*/
|
|
|
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import CodeGraph from '../src/index';
|
|
|
|
let tempDir: string;
|
|
let cg: CodeGraph | null = null;
|
|
|
|
async function callsFromMethod(source: string, methodName: string): Promise<string[]> {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1714-'));
|
|
fs.writeFileSync(path.join(tempDir, 'record.ts'), source);
|
|
cg = await CodeGraph.init(tempDir, { index: true });
|
|
cg.resolveReferences();
|
|
const from = cg.getNodesByKind('method').find((n) => n.name === methodName)!;
|
|
expect(from).toBeDefined();
|
|
return cg
|
|
.getOutgoingEdges(from.id)
|
|
.filter((e) => e.kind === 'calls')
|
|
.map((e) => cg!.getNode(e.target))
|
|
.filter((n): n is NonNullable<typeof n> => !!n)
|
|
.map((n) => `${n.kind}:${n.qualifiedName ?? n.name}`);
|
|
}
|
|
|
|
afterEach(() => {
|
|
cg?.close();
|
|
cg = null;
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('a receiver-less JS/TS call never binds to a method (#1714)', () => {
|
|
it('resolves the bare call onto the module-scope function, not the enclosing method', async () => {
|
|
const callees = await callsFromMethod(
|
|
[
|
|
'function serialize(value: string): string {',
|
|
' return value.trim();',
|
|
'}',
|
|
'',
|
|
'export class Record {',
|
|
' constructor(private readonly raw: string) {}',
|
|
' serialize(): string {',
|
|
' return serialize(this.raw);',
|
|
' }',
|
|
'}',
|
|
'',
|
|
].join('\n'),
|
|
'serialize'
|
|
);
|
|
expect(callees).toContain('function:serialize');
|
|
expect(callees).not.toContain('method:Record::serialize');
|
|
});
|
|
|
|
it('keeps `this.serialize()` — a real recursive self-call', async () => {
|
|
const callees = await callsFromMethod(
|
|
[
|
|
'function serialize(value: string): string {',
|
|
' return value.trim();',
|
|
'}',
|
|
'',
|
|
'export class Record {',
|
|
' constructor(private readonly raw: string, private depth = 0) {}',
|
|
' serialize(): string {',
|
|
' if (this.depth > 0) return this.serialize();',
|
|
' return this.raw;',
|
|
' }',
|
|
'}',
|
|
'',
|
|
].join('\n'),
|
|
'serialize'
|
|
);
|
|
expect(callees).toContain('method:Record::serialize');
|
|
});
|
|
|
|
it('a bare call to a name the file binds itself has no cross-file candidate', async () => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1714-'));
|
|
fs.writeFileSync(path.join(tempDir, 'config.ts'), 'export function resolve(p: string) { return p; }\nexport function transform(c: string) { return c; }\nexport function now() { return 0; }\n');
|
|
fs.writeFileSync(
|
|
path.join(tempDir, 'client.ts'),
|
|
[
|
|
'const transform = makeTransform();',
|
|
'export function ping(): Promise<void> {',
|
|
' return new Promise((resolve, reject) => {',
|
|
' setTimeout(() => resolve(), 10);',
|
|
' });',
|
|
'}',
|
|
'export function run(options: { now?: () => number }) {',
|
|
' const now = options.now || (() => Date.now());',
|
|
' return now() + transform("x").length;',
|
|
'}',
|
|
'',
|
|
].join('\n')
|
|
);
|
|
cg = await CodeGraph.init(tempDir, { index: true });
|
|
cg.resolveReferences();
|
|
const targets = cg.getNodesByKind('function').filter((n) => n.filePath === 'config.ts').map((n) => n.id);
|
|
const callers = cg.getNodesByKind('function').filter((n) => n.filePath === 'client.ts');
|
|
const crossFile = callers.flatMap((c) => cg!.getOutgoingEdges(c.id)).filter((e) => e.kind === 'calls' && targets.includes(e.target));
|
|
expect(crossFile).toEqual([]);
|
|
});
|
|
|
|
it('a destructured require or a string mentioning the name is not a local binding', async () => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1714-'));
|
|
fs.writeFileSync(path.join(tempDir, 'public-ip.js'), 'function lookupPublicIPv4() { return "1.2.3.4"; }\nfunction test(name, fn) { return fn(); }\nmodule.exports = { lookupPublicIPv4, test };\n');
|
|
fs.writeFileSync(
|
|
path.join(tempDir, 'main.js'),
|
|
[
|
|
'const { lookupPublicIPv4 } = require("./public-ip");',
|
|
'const { test } = require("./public-ip");',
|
|
'async function prepare() {',
|
|
' const ip = await lookupPublicIPv4();',
|
|
' test("a test of the thing", () => {});',
|
|
' return ip;',
|
|
'}',
|
|
'module.exports = { prepare };',
|
|
'',
|
|
].join('\n')
|
|
);
|
|
cg = await CodeGraph.init(tempDir, { index: true });
|
|
cg.resolveReferences();
|
|
const prepare = cg.getNodesByKind('function').find((n) => n.name === 'prepare')!;
|
|
const names = cg.getOutgoingEdges(prepare.id).filter((e) => e.kind === 'calls').map((e) => cg!.getNode(e.target)?.name);
|
|
expect(names).toContain('lookupPublicIPv4');
|
|
expect(names).toContain('test');
|
|
});
|
|
|
|
it('keeps `other.serialize()` — a call through a receiver', async () => {
|
|
const callees = await callsFromMethod(
|
|
[
|
|
'export class Record {',
|
|
' serialize(): string { return ""; }',
|
|
' copyOf(other: Record): string {',
|
|
' return other.serialize();',
|
|
' }',
|
|
'}',
|
|
'',
|
|
].join('\n'),
|
|
'copyOf'
|
|
);
|
|
expect(callees).toContain('method:Record::serialize');
|
|
});
|
|
});
|