Context
#791 introduced a reg/regCli registrar split for VS Code commands in packages/vscode/src/extension.ts. The pattern's appeal is that the registrar name at each call site IS the guard policy — no parallel list to keep in sync. The two helpers:
const reg = <A>(id: string, handler: (...args: A) => unknown) =>
vscode.commands.registerCommand(id, handler);
const regCli = <A>(id: string, handler: (...args: A) => unknown) =>
vscode.commands.registerCommand(id, guard(handler)); // guard = CLI-ready check + setup toast
All 44 command registrations in extension.ts flow through one of these two helpers today.
Problem
The convention is a habit, not an enforced rule. A future contributor adding a command via the older form:
vscode.commands.registerCommand('codev.newThing', () => doThing())
silently bypasses both guards. The CLI-preflight signal that #791 paid for never reaches the new command, and the only signal at review time is grep — easy to miss in a larger PR.
This is a small but real silent-regression class. Worth one-time enforcement.
Proposed change
Add a custom ESLint rule (or no-restricted-syntax selector — simpler if it covers the case) to the vscode package's lint config:
- Ban:
vscode.commands.registerCommand(...) — bare calls.
- Allow:
reg(...), regCli(...) — the two helpers.
- Allow internally: the helpers themselves (the two definitions in
extension.ts that call vscode.commands.registerCommand). Either by scoping the rule to disallow the call outside a function body named reg/regCli, or by exempting extension.ts lines containing the helper definitions, or via an inline eslint-disable-next-line on the two helper bodies (simplest, and visible).
The rule lives in packages/vscode/eslint.config.js (or the workspace config if that's where vscode-package overrides go).
Rule shape (sketch — no-restricted-syntax likely sufficient)
{
selector: \"CallExpression[callee.object.object.name='vscode'][callee.object.property.name='commands'][callee.property.name='registerCommand']\",
message: 'Use reg(...) or regCli(...) from extension.ts instead of bare vscode.commands.registerCommand (see #791).',
}
Two eslint-disable-next-line on the helper bodies themselves (visible, intentional escape hatches).
Alternative considered
A full custom rule package (packages/vscode/eslint-rules/no-bare-register-command.js with its own AST visitor) would be more precise but requires standing up custom-rule plumbing in eslint config (loaders, plugin shape). no-restricted-syntax is built-in, zero additional config, and the selector above is precise enough.
Acceptance
Why AIR
Small (<50 LOC change, mostly config), no architectural decisions, no UI to test, no plan worth writing. Implement → review fits cleanly.
Related
Context
#791 introduced a
reg/regCliregistrar split for VS Code commands inpackages/vscode/src/extension.ts. The pattern's appeal is that the registrar name at each call site IS the guard policy — no parallel list to keep in sync. The two helpers:All 44 command registrations in
extension.tsflow through one of these two helpers today.Problem
The convention is a habit, not an enforced rule. A future contributor adding a command via the older form:
silently bypasses both guards. The CLI-preflight signal that #791 paid for never reaches the new command, and the only signal at review time is grep — easy to miss in a larger PR.
This is a small but real silent-regression class. Worth one-time enforcement.
Proposed change
Add a custom ESLint rule (or
no-restricted-syntaxselector — simpler if it covers the case) to the vscode package's lint config:vscode.commands.registerCommand(...)— bare calls.reg(...),regCli(...)— the two helpers.extension.tsthat callvscode.commands.registerCommand). Either by scoping the rule to disallow the call outside a function body namedreg/regCli, or by exemptingextension.tslines containing the helper definitions, or via an inlineeslint-disable-next-lineon the two helper bodies (simplest, and visible).The rule lives in
packages/vscode/eslint.config.js(or the workspace config if that's where vscode-package overrides go).Rule shape (sketch —
no-restricted-syntaxlikely sufficient)Two
eslint-disable-next-lineon the helper bodies themselves (visible, intentional escape hatches).Alternative considered
A full custom rule package (
packages/vscode/eslint-rules/no-bare-register-command.jswith its own AST visitor) would be more precise but requires standing up custom-rule plumbing in eslint config (loaders, plugin shape).no-restricted-syntaxis built-in, zero additional config, and the selector above is precise enough.Acceptance
vscode.commands.registerCommand('codev.foo', () => ...)call anywhere inpackages/vscode/src/(outside the two helper definitions) failspnpm lint.reg('codev.foo', () => ...)orregCli('codev.foo', () => ...)lints cleanly.extension.tscontinue to lint cleanly (viaeslint-disable-next-lineor selector scoping).Why AIR
Small (<50 LOC change, mostly config), no architectural decisions, no UI to test, no plan worth writing. Implement → review fits cleanly.
Related