🧹 Fix unsafe type coercions in HostBridge toast notifications - #132
Conversation
Co-authored-by: zknpr <96851588+zknpr@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the type safety and robustness of the HostBridge's toast notification system. By replacing unsafe type coercions with explicit mapping logic, the integration with VS Code's native message APIs is made more reliable, ensuring that custom dialog configurations and buttons are correctly translated and handled. This change improves code health and reduces the risk of runtime errors due to type mismatches. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request effectively removes unsafe as any type coercions from the toast notification methods, enhancing type safety. The new implementation, however, introduces significant code duplication across showInformationToast, showWarningToast, and showErrorToast. I've provided a suggestion to refactor this repeated logic into a private helper method, which will improve the code's maintainability.
| async showInformationToast<T extends string | DialogButton>(message: string, options?: DialogConfig, ...items: T[]): Promise<T | undefined> { | ||
| return await vsc.window.showInformationMessage(message, options as any, ...items as any[]); | ||
| const vscOptions = this.mapOptions(options); | ||
| if (items.length > 0 && typeof items[0] !== 'string') { | ||
| const buttons = items as unknown as DialogButton[]; | ||
| const vscItems = buttons.map(b => ({ title: b.caption, isCloseAffordance: b.isCloseAction })); | ||
| const result = await vsc.window.showInformationMessage(message, vscOptions, ...vscItems); | ||
| return result ? buttons.find(b => b.caption === result.title) as unknown as T : undefined; | ||
| } | ||
| return await vsc.window.showInformationMessage(message, vscOptions, ...(items as unknown as string[])) as unknown as T; | ||
| } | ||
|
|
||
| /** | ||
| * Show a warning toast message. | ||
| */ | ||
| async showWarningToast<T extends string | DialogButton>(message: string, options?: DialogConfig, ...items: T[]): Promise<T | undefined> { | ||
| return await vsc.window.showWarningMessage(message, options as any, ...items as any[]); | ||
| const vscOptions = this.mapOptions(options); | ||
| if (items.length > 0 && typeof items[0] !== 'string') { | ||
| const buttons = items as unknown as DialogButton[]; | ||
| const vscItems = buttons.map(b => ({ title: b.caption, isCloseAffordance: b.isCloseAction })); | ||
| const result = await vsc.window.showWarningMessage(message, vscOptions, ...vscItems); | ||
| return result ? buttons.find(b => b.caption === result.title) as unknown as T : undefined; | ||
| } | ||
| return await vsc.window.showWarningMessage(message, vscOptions, ...(items as unknown as string[])) as unknown as T; | ||
| } | ||
|
|
||
| /** | ||
| * Show an error toast message. | ||
| */ | ||
| async showErrorToast<T extends string | DialogButton>(message: string, options?: DialogConfig, ...items: T[]): Promise<T | undefined> { | ||
| return await vsc.window.showErrorMessage(message, options as any, ...items as any[]); | ||
| const vscOptions = this.mapOptions(options); | ||
| if (items.length > 0 && typeof items[0] !== 'string') { | ||
| const buttons = items as unknown as DialogButton[]; | ||
| const vscItems = buttons.map(b => ({ title: b.caption, isCloseAffordance: b.isCloseAction })); | ||
| const result = await vsc.window.showErrorMessage(message, vscOptions, ...vscItems); | ||
| return result ? buttons.find(b => b.caption === result.title) as unknown as T : undefined; | ||
| } | ||
| return await vsc.window.showErrorMessage(message, vscOptions, ...(items as unknown as string[])) as unknown as T; | ||
| } |
There was a problem hiding this comment.
The logic for showInformationToast, showWarningToast, and showErrorToast is nearly identical. To improve maintainability and follow the DRY (Don't Repeat Yourself) principle, this duplicated code can be extracted into a single private helper method. This helper can accept the specific show...Message function from vscode.window as an argument, making the public methods simple one-line calls to this new helper.
private async _showToast<T extends string | DialogButton>(
show: {
(message: string, options: vsc.MessageOptions, ...items: vsc.MessageItem[]): Promise<vsc.MessageItem | undefined>;
(message: string, options: vsc.MessageOptions, ...items: string[]): Promise<string | undefined>;
},
message: string,
options?: DialogConfig,
...items: T[]
): Promise<T | undefined> {
const vscOptions = this.mapOptions(options);
if (items.length > 0 && typeof items[0] !== 'string') {
const buttons = items as unknown as DialogButton[];
const vscItems = buttons.map(b => ({ title: b.caption, isCloseAffordance: b.isCloseAction }));
const result = await show(message, vscOptions, ...vscItems);
return result ? buttons.find(b => b.caption === result.title) as unknown as T : undefined;
}
const result = await show(message, vscOptions, ...(items as unknown as string[]));
return result as unknown as T;
}
/**
* Show an information toast message.
*/
async showInformationToast<T extends string | DialogButton>(message: string, options?: DialogConfig, ...items: T[]): Promise<T | undefined> {
return this._showToast(vsc.window.showInformationMessage, message, options, ...items);
}
/**
* Show a warning toast message.
*/
async showWarningToast<T extends string | DialogButton>(message: string, options?: DialogConfig, ...items: T[]): Promise<T | undefined> {
return this._showToast(vsc.window.showWarningMessage, message, options, ...items);
}
/**
* Show an error toast message.
*/
async showErrorToast<T extends string | DialogButton>(message: string, options?: DialogConfig, ...items: T[]): Promise<T | undefined> {
return this._showToast(vsc.window.showErrorMessage, message, options, ...items);
}
Greptile SummaryThis PR removes Key observations:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Caller
participant HostBridge
participant mapOptions
participant vscode as VS Code Window API
Caller->>HostBridge: showInformationToast(message, options?, ...items)
HostBridge->>mapOptions: mapOptions(options?)
mapOptions-->>HostBridge: { modal, detail }
alt items present and first item is not a string (DialogButton path)
HostBridge->>HostBridge: cast items → DialogButton[]
HostBridge->>HostBridge: map buttons → { title, isCloseAffordance }[]
HostBridge->>vscode: showInformationMessage(message, vscOptions, ...vscItems)
vscode-->>HostBridge: result (MessageItem | undefined)
HostBridge->>HostBridge: find original DialogButton where caption === result.title
HostBridge-->>Caller: matched DialogButton as T | undefined
else items are strings (or empty)
HostBridge->>vscode: showInformationMessage(message, vscOptions, ...strings)
vscode-->>HostBridge: result (string | undefined)
HostBridge-->>Caller: string as T | undefined
end
Last reviewed commit: 8c5a613 |
| async showInformationToast<T extends string | DialogButton>(message: string, options?: DialogConfig, ...items: T[]): Promise<T | undefined> { | ||
| return await vsc.window.showInformationMessage(message, options as any, ...items as any[]); | ||
| const vscOptions = this.mapOptions(options); | ||
| if (items.length > 0 && typeof items[0] !== 'string') { | ||
| const buttons = items as unknown as DialogButton[]; | ||
| const vscItems = buttons.map(b => ({ title: b.caption, isCloseAffordance: b.isCloseAction })); | ||
| const result = await vsc.window.showInformationMessage(message, vscOptions, ...vscItems); | ||
| return result ? buttons.find(b => b.caption === result.title) as unknown as T : undefined; | ||
| } | ||
| return await vsc.window.showInformationMessage(message, vscOptions, ...(items as unknown as string[])) as unknown as T; | ||
| } | ||
|
|
||
| /** | ||
| * Show a warning toast message. | ||
| */ | ||
| async showWarningToast<T extends string | DialogButton>(message: string, options?: DialogConfig, ...items: T[]): Promise<T | undefined> { | ||
| return await vsc.window.showWarningMessage(message, options as any, ...items as any[]); | ||
| const vscOptions = this.mapOptions(options); | ||
| if (items.length > 0 && typeof items[0] !== 'string') { | ||
| const buttons = items as unknown as DialogButton[]; | ||
| const vscItems = buttons.map(b => ({ title: b.caption, isCloseAffordance: b.isCloseAction })); | ||
| const result = await vsc.window.showWarningMessage(message, vscOptions, ...vscItems); | ||
| return result ? buttons.find(b => b.caption === result.title) as unknown as T : undefined; | ||
| } | ||
| return await vsc.window.showWarningMessage(message, vscOptions, ...(items as unknown as string[])) as unknown as T; | ||
| } | ||
|
|
||
| /** | ||
| * Show an error toast message. | ||
| */ | ||
| async showErrorToast<T extends string | DialogButton>(message: string, options?: DialogConfig, ...items: T[]): Promise<T | undefined> { | ||
| return await vsc.window.showErrorMessage(message, options as any, ...items as any[]); | ||
| const vscOptions = this.mapOptions(options); | ||
| if (items.length > 0 && typeof items[0] !== 'string') { | ||
| const buttons = items as unknown as DialogButton[]; | ||
| const vscItems = buttons.map(b => ({ title: b.caption, isCloseAffordance: b.isCloseAction })); | ||
| const result = await vsc.window.showErrorMessage(message, vscOptions, ...vscItems); | ||
| return result ? buttons.find(b => b.caption === result.title) as unknown as T : undefined; | ||
| } | ||
| return await vsc.window.showErrorMessage(message, vscOptions, ...(items as unknown as string[])) as unknown as T; | ||
| } |
There was a problem hiding this comment.
Duplicated button-handling logic across all three toast methods
The identical if (items.length > 0 && typeof items[0] !== 'string') block — including the buttons cast, vscItems mapping, and find-based result lookup — is copy-pasted verbatim into showInformationToast, showWarningToast, and showErrorToast. Any future bug fix or enhancement (e.g. changing how button captions are matched) would need to be applied in three places.
Consider extracting a private helper such as:
private async showToast<T extends string | DialogButton>(
fn: typeof vsc.window.showInformationMessage,
message: string,
options: import('vscode').MessageOptions,
items: T[]
): Promise<T | undefined> {
if (items.length > 0 && typeof items[0] !== 'string') {
const buttons = items as unknown as DialogButton[];
const vscItems = buttons.map(b => ({ title: b.caption, isCloseAffordance: b.isCloseAction }));
const result = await fn(message, options, ...vscItems);
return result ? buttons.find(b => b.caption === result.title) as unknown as T : undefined;
}
return await fn(message, options, ...(items as unknown as string[])) as unknown as T;
}and then delegate from each public method.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| private mapOptions(options?: DialogConfig): import('vscode').MessageOptions { | ||
| return { | ||
| modal: options?.modal, | ||
| detail: options?.detailText | ||
| }; | ||
| } |
There was a problem hiding this comment.
mapOptions always returns a non-undefined object
When options is undefined, mapOptions returns { modal: undefined, detail: undefined } — a real object. This is always forwarded as the MessageOptions argument, meaning VS Code always sees the overload that accepts explicit options rather than the bare (message, ...items) overload. In practice VS Code treats undefined properties as absent, so this is functionally equivalent, but it does represent a subtle behavioural change from the original code (which passed undefined directly).
A small guard makes the intent explicit:
| private mapOptions(options?: DialogConfig): import('vscode').MessageOptions { | |
| return { | |
| modal: options?.modal, | |
| detail: options?.detailText | |
| }; | |
| } | |
| private mapOptions(options?: DialogConfig): import('vscode').MessageOptions { | |
| return { | |
| modal: options?.modal, | |
| detail: options?.detailText | |
| }; | |
| } |
No action required if this is intentional, but worth documenting with a comment explaining why an empty-ish options object is always passed.
| const buttons = items as unknown as DialogButton[]; | ||
| const vscItems = buttons.map(b => ({ title: b.caption, isCloseAffordance: b.isCloseAction })); | ||
| const result = await vsc.window.showInformationMessage(message, vscOptions, ...vscItems); | ||
| return result ? buttons.find(b => b.caption === result.title) as unknown as T : undefined; |
There was a problem hiding this comment.
Button lookup assumes unique captions
buttons.find(b => b.caption === result.title) returns the first DialogButton whose caption matches the VS Code title. If two buttons happen to share the same caption, the user clicking the second one will silently return the first DialogButton, causing incorrect behaviour.
This assumption is safe when callers ensure unique captions, but it is not enforced anywhere. Consider adding a comment documenting the uniqueness requirement, or switching to an index-based lookup:
const idx = vscItems.findIndex(v => v.title === result!.title);
return idx !== -1 ? buttons[idx] as unknown as T : undefined;This pattern is repeated in showWarningToast (line 704) and showErrorToast (line 718) as well.
|
Implemented manually on main with a shared |
Understood. Acknowledging that this work was implemented manually on main. |
…d code, bump sql.js - Replace `as any` casts in HostBridge toast methods with shared `showToast` helper that properly maps DialogConfig→MessageOptions and DialogButton→MessageItem (PR #132) - Extract getMaximumFileSizeBytes and getQueryTimeout from workerFactory.ts to config.ts for testability without import.meta.env (PR #135) - Remove dead rowIds filtering code in tableExporter non-rowid branch (PR #137) - Bump sql.js from 1.13.0 to 1.14.0 (PR #112) - Add configurable mock store to vscode test mock for workspace.getConfiguration - Add unit tests for getMaximumFileSizeBytes and getQueryTimeout Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🎯 What: Removed unsafe
as anycasts inshowInformationToast,showWarningToast, andshowErrorToastmethods ofsrc/hostBridge.ts.💡 Why: The existing code used
anyto bypass TypeScript's compiler checks when passing customDialogConfigandDialogButtonobjects to VS Code's nativeMessageOptionsandMessageItemAPIs. The new approach uses amapOptionshelper and object mapping to strictly adhere to the expected VS Code types, greatly improving codebase health, reliability, and catching potential future API mismatches.✅ Verification: Verified by compiling via
bun run buildand runningbun test tests/unit/hostBridge.test.ts. Also passed automated code review confirming proper behavior and memory cleanup.✨ Result: A fully type-safe implementation in the VS Code bridge layer that translates application domain models seamlessly into native VS Code properties (
modal,detail,title,isCloseAffordance) and correctly returns the matched model on user selection.PR created automatically by Jules for task 831020638428175877 started by @zknpr