mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 03:49:59 +08:00
A receiver-less JS/TS call written as the body of a braceless if, while, for or do, like `if (e.key === "Escape") setOpen(false);`, is now read as a bare call. In the JS family a receiver ends in `.` or `?.`, so a `)` right before a call's name can only close a statement header; `do` joins the bare-call keywords. Go keeps its old reading. Before, such a call skipped the bare-call guards and could link to another file's same-named member, like a context interface's `setOpen` property. Fixes issue 2585. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
4.6 KiB
TypeScript
140 lines
4.6 KiB
TypeScript
/**
|
|
* A receiver-less call that is the body of a braceless `if`, `while`, `for` or
|
|
* `do` — `if (e.key === "Escape") setOpen(false);` — is still a bare call
|
|
* (#2585), also when the header wraps lines or holds a `)` in a string.
|
|
* The character before the name is the `)` that closes the statement's header,
|
|
* and that `)` used to read as the end of a receiver. The call then skipped
|
|
* both guards a bare JS/TS call gets: no type member, and no other file's
|
|
* symbol when the file binds the name itself. A `setOpen` from
|
|
* `const [open, setOpen] = useState(false)` linked to a context interface's
|
|
* `setOpen` property in another file.
|
|
*/
|
|
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';
|
|
|
|
const CTX = `export interface PaletteCtx {
|
|
open: boolean;
|
|
setOpen: (v: boolean) => void;
|
|
}
|
|
`;
|
|
|
|
const DIALOG = `import { useState } from "react";
|
|
export function Dialog(xs: number[]) {
|
|
const [open, setOpen] = useState(false);
|
|
function a(e: KeyboardEvent) {
|
|
if (e.key === "Escape") setOpen(false);
|
|
}
|
|
function b() {
|
|
setOpen(false);
|
|
}
|
|
function c(e: KeyboardEvent) {
|
|
if (e.key === "Escape") { setOpen(false); }
|
|
}
|
|
function w() {
|
|
while (xs.length > 99) setOpen(false);
|
|
}
|
|
function f() {
|
|
for (const x of xs) setOpen(x > 0);
|
|
}
|
|
function g() {
|
|
if (open) { setOpen(false); } else setOpen(true);
|
|
}
|
|
async function h(src: AsyncIterable<number>) {
|
|
for await (const x of src) setOpen(x > 0);
|
|
}
|
|
function k(e: KeyboardEvent) {
|
|
if (e.key === "Escape" && (open || xs.length)) setOpen(false);
|
|
}
|
|
function m(s: string) {
|
|
if (xs.length > 0 &&
|
|
open) setOpen(false);
|
|
if (s === ")") setOpen(true);
|
|
if (/\\)/.test(s)) setOpen(false);
|
|
if (\`\${s})\` === s) setOpen(true);
|
|
if (s === "(") setOpen(false);
|
|
}
|
|
function n() {
|
|
do setOpen(false); while (open);
|
|
}
|
|
return [a, b, c, w, f, g, h, k, m, n];
|
|
}
|
|
`;
|
|
|
|
describe('a call that is the body of a braceless if/while/for/do (#2585)', () => {
|
|
let tmpDir: string | undefined;
|
|
let cg: CodeGraph | undefined;
|
|
|
|
afterEach(() => {
|
|
cg?.close();
|
|
cg = undefined;
|
|
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5 });
|
|
tmpDir = undefined;
|
|
});
|
|
|
|
/** `calls` targets of the function `name`, as `<kind>:<name>`. */
|
|
const callees = (graph: CodeGraph, name: string): string[] => {
|
|
const from = graph.getNodesByName(name).find((n) => n.kind === 'function')!;
|
|
return graph.getOutgoingEdges(from.id).filter((e) => e.kind === 'calls')
|
|
.map((e) => graph.getNode(e.target)!)
|
|
.map((n) => `${n.kind}:${n.name}`);
|
|
};
|
|
|
|
it('is a bare call: it never reaches another file\'s type member', async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-braceless-'));
|
|
fs.mkdirSync(path.join(tmpDir, 'src'));
|
|
fs.writeFileSync(path.join(tmpDir, 'src/ctx.ts'), CTX);
|
|
fs.writeFileSync(path.join(tmpDir, 'src/Dialog.tsx'), DIALOG);
|
|
cg = await CodeGraph.init(tmpDir, { index: true });
|
|
|
|
for (const fn of ['a', 'b', 'c', 'w', 'f', 'g', 'h', 'k', 'm', 'n']) {
|
|
expect(callees(cg, fn), fn).not.toContain('property:setOpen');
|
|
}
|
|
});
|
|
|
|
it('still resolves a bare call after a braceless header to its own target', async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-braceless-'));
|
|
fs.writeFileSync(path.join(tmpDir, 'util.ts'), `export function helper() { return 1; }
|
|
export function use(open: boolean) {
|
|
if (open) helper();
|
|
}
|
|
`);
|
|
cg = await CodeGraph.init(tmpDir, { index: true });
|
|
|
|
expect(callees(cg, 'use')).toEqual(['function:helper']);
|
|
});
|
|
|
|
it('still resolves a call through a parenthesized receiver', async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-braceless-'));
|
|
fs.writeFileSync(path.join(tmpDir, 'svc.ts'), `export class Svc {
|
|
run() { return 1; }
|
|
}
|
|
export function make(): Svc { return new Svc(); }
|
|
export function use() {
|
|
return (make()).run();
|
|
}
|
|
`);
|
|
cg = await CodeGraph.init(tmpDir, { index: true });
|
|
|
|
expect(callees(cg, 'use')).toContain('function:make');
|
|
});
|
|
|
|
it('still resolves a call on a receiver after a braceless header', async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-braceless-'));
|
|
fs.writeFileSync(path.join(tmpDir, 'svc.ts'), `export class Svc {
|
|
setOpen(v: boolean) { return v; }
|
|
run() { return 1; }
|
|
}
|
|
export function use(svc: Svc, open: boolean) {
|
|
if (open) svc.setOpen(false);
|
|
while (open) svc.run();
|
|
}
|
|
`);
|
|
cg = await CodeGraph.init(tmpDir, { index: true });
|
|
|
|
expect(callees(cg, 'use').sort()).toEqual(['method:run', 'method:setOpen']);
|
|
});
|
|
});
|