mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-21 00:38:18 +08:00
fix(resolution): a call that is a braceless if/while/for/do body is a bare call (#2592)
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>
This commit is contained in:
+1
-1
@@ -14,8 +14,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- In JavaScript and TypeScript, a call written as the body of an `if`, `while`, `for` or `do` without braces, like `if (e.key === "Escape") setOpen(false);`, no longer links to a same-named property or method in another file when the name is the file's own, such as a `setOpen` from `useState` (#2585).
|
||||
- `codegraph sync` no longer fails with "FOREIGN KEY constraint failed" in a project whose code lazy-loads a workspace package, like Supabase Studio's `dynamic(() => import('icons').then((mod) => mod.EdgeFunctions))` or vben's `() => import('@vben/layouts').then((m) => m.IFrameView)`. Before, every sync after an edit in such a project rolled back, so the index stopped following your changes until a full re-index. Thanks @bompus for the report and @inth3shadows. (#2565)
|
||||
|
||||
- In VB.NET, every member of a `Structure` is now indexed, including its fields, properties, methods, constructors and nested enums. Before, only the first member was, so the rest could not be found and their callers looked empty.
|
||||
- In VB.NET and C#, what a property's `Get` and `Set` code calls, creates and reads now belongs to that property, as do C#'s `get => …` accessors and `=> …` property bodies. Before, it was dropped, so a method used only from a property looked unused.
|
||||
- In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading.
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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']);
|
||||
});
|
||||
});
|
||||
@@ -3473,6 +3473,10 @@ function isReceiverLessCall(ref: UnresolvedRef, context: ResolutionContext): boo
|
||||
let end = Math.min(ref.column, line.length);
|
||||
while (end > 0 && WHITESPACE.test(line[end - 1]!)) end--;
|
||||
if (end === 0 || !RECEIVER_TAIL_CHAR.test(line[end - 1]!)) return true;
|
||||
// In JS/TS a receiver ends in `.` or `?.`, so a `)` right before the name
|
||||
// closes a statement header — `if (open) setOpen()`, `for (…) f()`, a header
|
||||
// that wraps lines or holds a `)` in a string — and the call is bare (#2585).
|
||||
if (line[end - 1] === ')' && JS_FAMILY.has(ref.language)) return true;
|
||||
let start = end;
|
||||
while (start > 0 && WORD_CHAR.test(line[start - 1]!)) start--;
|
||||
return BARE_CALL_KEYWORDS.has(line.slice(start, end));
|
||||
@@ -3490,7 +3494,7 @@ const RECEIVER_TAIL_CHAR = /[.\w$\])]/;
|
||||
*/
|
||||
const BARE_CALL_KEYWORDS: ReadonlySet<string> = new Set([
|
||||
'return', 'await', 'yield', 'typeof', 'void', 'new', 'else', 'case', 'throw', 'in', 'of', 'instanceof', 'go', 'defer',
|
||||
'if', 'switch', 'for', 'range',
|
||||
'if', 'switch', 'for', 'range', 'do',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user