From 277a3096e0debfd9883d9a771198a205c9de9e02 Mon Sep 17 00:00:00 2001 From: Eric Wheeler Date: Fri, 16 May 2025 21:26:14 -0700 Subject: [PATCH 1/4] feat: Enforce explicit type coercion and empty blocks rules Add no-implicit-coercion and no-empty rules to improve code quality across the codebase: - no-implicit-coercion: Enforces explicit, context-aware type comparisons to address ambiguity issues with falsy values and improve code clarity - no-empty: Requires all code blocks to have meaningful content, preventing silent error suppression and improving code readability This change includes: - Adding detailed documentation in .roo/rules/eslint-no-implicit-coercion.md with specific guidance on proper type checking patterns - Adding documentation for no-unused-vars rule enforcement in .roo/rules/eslint-no-unused-vars.md - Explicitly adding both rules as "error" in both root and webview-ui configs - Setting --max-warnings=0 in lint scripts to ensure strict enforcement These changes ensure developers use explicit type checks and properly document empty blocks, particularly important for distinguishing between null, undefined, and empty strings in conditional logic. Fixes: #3692 Signed-off-by: Eric Wheeler --- .roo/rules/eslint-no-empty.md | 38 +++++++++++ .roo/rules/eslint-no-implicit-coercion.md | 79 +++++++++++++++++++++++ .roo/rules/eslint-no-unused-vars.md | 63 ++++++++++++++++++ src/eslint.config.mjs | 4 +- webview-ui/eslint.config.mjs | 2 + 5 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 .roo/rules/eslint-no-empty.md create mode 100644 .roo/rules/eslint-no-implicit-coercion.md create mode 100644 .roo/rules/eslint-no-unused-vars.md diff --git a/.roo/rules/eslint-no-empty.md b/.roo/rules/eslint-no-empty.md new file mode 100644 index 0000000000..6cef399dea --- /dev/null +++ b/.roo/rules/eslint-no-empty.md @@ -0,0 +1,38 @@ +# Handling ESLint `no-empty` Rule + +Empty block statements (`{}`) are disallowed. All blocks MUST include a logging statement (e.g., `console.error`, `console.warn`, `console.log`, `console.debug`) to make their purpose explicit. + +This applies to `if`, `else`, `while`, `for`, `switch` cases, `try...catch` blocks, and function bodies. + +### Examples: + +```javascript +// Correct: Logging in blocks +if (condition) { + console.warn("Condition met, no specific action.") +} + +try { + criticalOperation() +} catch (error) { + // For unexpected errors: + console.error("Unexpected error in criticalOperation:", error) +} + +function foo() { + console.log("foo called, no operation.") +} +``` + +### Special Considerations: + +- **Intentional Error Suppression**: Use `console.debug` in `catch` blocks if an error is intentionally suppressed. + ```javascript + try { + operationThatMightBenignlyFail() + } catch (error) { + console.debug("Benign failure in operation, suppressed:", error) + } + ``` +- **Constructors**: Empty constructors should log, preferably with `console.debug`. +- **Comments**: Comments can supplement logs but do not replace the logging requirement. diff --git a/.roo/rules/eslint-no-implicit-coercion.md b/.roo/rules/eslint-no-implicit-coercion.md new file mode 100644 index 0000000000..9c88814f45 --- /dev/null +++ b/.roo/rules/eslint-no-implicit-coercion.md @@ -0,0 +1,79 @@ +# Handling ESLint `no-implicit-coercion` Errors + +When ESLint's `no-implicit-coercion` rule flags an error, a value is being implicitly converted to a boolean, number, or string. While ESLint might suggest `Boolean(value)`, `Number(value)`, or `String(value)`, this project requires a different approach for boolean coercions. + +## Guideline: Explicit, Context-Aware Comparisons + +For boolean coercions (e.g., in `if` statements or ternaries), MUST NOT use `Boolean(value)`. Instead, extend conditional expressions to explicitly compare against the specific data type and value expected for that implementation context. + +YOU MUST NEVER replace `if (myVar)` with `if (Boolean(myVar))` or `if (!!myVar)` with `if (Boolean(myVar))`. + +This rule's purpose is to encourage a thoughtful evaluation of what "truthy" or "falsy" means for the specific variable and logic. + +### Examples: + +## Incorrect (MUST NOT DO THIS): + +```typescript +// Implicit coercion (flagged by ESLint) +if (someStringOrNull) { + // ... +} + +// Explicit coercion using Boolean() (not the preferred fix here) +if (Boolean(someStringOrNull)) { + // ... +} +``` + +## Correct (Preferred Approach): + +- Checking for a non-empty string: + + ```typescript + if (typeof someStringOrNull === "string" && someStringOrNull != "") { + // ... + } + // Or, if an empty string is valid but null/undefined is not: + if (typeof someStringOrNull === "string") { + // ... + } + ``` + +- Checking against `null` or `undefined`: + + ```typescript + if (someValue !== null && someValue !== undefined) { + // ... + } + // Shorter (catches both null and undefined, mind other falsy values): + if (someValue != null) { + // ... + } + ``` + +- Checking if a variable is assigned (not `undefined`): + + ```typescript + if (someOptionalValue !== undefined) { + // ... + } + ``` + +- Checking if an array has elements: + ```typescript + if (Array.isArray(myArray) && myArray.length > 0) { + // ... + } + ``` + +### Rationale: + +Explicitly comparing types and values: + +1. Clarifies the code's intent. +2. Reduces ambiguity regarding how falsy values (`null`, `undefined`, `0`, `""`, `NaN`) are handled. +3. Helps avoid bugs from overly general truthiness checks when specific conditions were needed. +4. Be careful of regular expression evaluations. For example, `/foo (.*)bar/` will legitimately match an empty string, or it may not match at all. You MUST differentiate between `match === undefined` vs `typeof match === 'string' && match != ""` because falsy evaluation MUST NOT BE USED because it is usually invalid and certainly imprecise. + +Always consider the context: What does "truthy" or "falsy" mean for this variable in this logic? Write conditions reflecting that precise meaning. diff --git a/.roo/rules/eslint-no-unused-vars.md b/.roo/rules/eslint-no-unused-vars.md new file mode 100644 index 0000000000..a48d3e64a4 --- /dev/null +++ b/.roo/rules/eslint-no-unused-vars.md @@ -0,0 +1,63 @@ +# Handling `@typescript-eslint/no-unused-vars` + +If `@typescript-eslint/no-unused-vars` flags an error, a declaration is unused. + +## Guideline: Omit or Delete + +Unused declarations MUST be omitted or deleted. + +CRITICAL: Unused declarations MUST NOT be "fixed" by prefixing with an underscore (`_`). This is disallowed. Remove dead code, do not silence the linter. + +### Examples: + +#### Incorrect: + +```typescript +// error: 'unusedVar' is defined but never used. +function example(usedParam: string, unusedParam: number): void { + console.log(usedParam) + // Incorrect fix: + // const _unusedVar = 10; +} +``` + +```typescript +// error: 'error' is defined but never used. +try { + // ... +} catch (error) { + // 'error' is unused + // Incorrect fix: + // } catch (_error) { + console.error("An operation failed.") +} +``` + +#### Correct: + +```typescript +// 'unusedParam' removed if not needed by an interface/override. +function example(usedParam: string): void { + // 'unusedParam' removed + console.log(usedParam) + // 'unusedVar' is completely removed. +} +``` + +```typescript +// 'error' variable is removed from the catch block if not used. +try { + // ... +} catch { + // 'error' variable omitted entirely + console.error("An operation failed.") +} +``` + +### Rationale: + +- Clarity: Removing unused code improves readability. +- Bugs: Unused variables can indicate errors. +- No Workarounds: Prefixing with `_` hides issues. + +Code should be actively used. diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index d0813406d9..7147b6f3ea 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -8,9 +8,11 @@ export default [ // TODO: These should be fixed and the rules re-enabled. "no-regex-spaces": "off", "no-useless-escape": "off", - "no-empty": "off", "prefer-const": "off", + "no-empty": "error", + "no-implicit-coercion": "error", + "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-require-imports": "off", diff --git a/webview-ui/eslint.config.mjs b/webview-ui/eslint.config.mjs index db76f49211..48836dca69 100644 --- a/webview-ui/eslint.config.mjs +++ b/webview-ui/eslint.config.mjs @@ -18,6 +18,8 @@ export default [ "@typescript-eslint/no-explicit-any": "off", "react/prop-types": "off", "react/display-name": "off", + "no-empty": "error", + "no-implicit-coercion": "error", }, }, { From 2ebad97516b67dc8571b4c1a72d2ed7c06fcb4f0 Mon Sep 17 00:00:00 2001 From: Eric Wheeler Date: Mon, 2 Jun 2025 14:36:54 -0700 Subject: [PATCH 2/4] feat: Add no-useless-catch ESLint rule Enforce the `no-useless-catch` ESLint rule across the codebase. This rule disallows `catch` clauses that only rethrow the caught error, which provides no additional value and can make debugging harder. This change adds the rule as an "error" to: - The root ESLint configuration ([](/.eslintrc.json)) - The webview UI ESLint configuration ([](webview-ui/.eslintrc.json)) This ensures that all `catch` blocks either handle the error meaningfully or are omitted if the error should propagate. Signed-off-by: Eric Wheeler --- src/eslint.config.mjs | 1 + webview-ui/eslint.config.mjs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index 7147b6f3ea..4f70db5753 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -12,6 +12,7 @@ export default [ "no-empty": "error", "no-implicit-coercion": "error", + "no-useless-catch": "error", "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-explicit-any": "off", diff --git a/webview-ui/eslint.config.mjs b/webview-ui/eslint.config.mjs index 48836dca69..035a8e6a2e 100644 --- a/webview-ui/eslint.config.mjs +++ b/webview-ui/eslint.config.mjs @@ -20,6 +20,7 @@ export default [ "react/display-name": "off", "no-empty": "error", "no-implicit-coercion": "error", + "no-useless-catch": "error", }, }, { From 66f563d192fd8740b4d0dc1a354a1f0596a91ab7 Mon Sep 17 00:00:00 2001 From: Eric Wheeler Date: Mon, 2 Jun 2025 15:51:55 -0700 Subject: [PATCH 3/4] fix: replace implicit coercions with explicit boolean checks Replace implicit boolean coercions (git diff --staged | catvariable) with explicit type-aware checks to satisfy the no-implicit-coercion ESLint rule. Also add debug logging to empty blocks to satisfy the no-empty rule. - Use type-specific checks (typeof x === 'string') for string variables - Use explicit comparisons (x === true) for boolean variables - Remove redundant null checks for variables that can't be null - Add console.debug statements to empty catch blocks Signed-off-by: Eric Wheeler --- src/api/providers/__tests__/openai.spec.ts | 2 ++ src/api/providers/vscode-lm.ts | 2 +- src/core/condense/index.ts | 4 ++-- src/core/config/ContextProxy.ts | 2 +- src/core/config/importExport.ts | 4 +++- src/core/tools/executeCommandTool.ts | 11 +++++++++-- src/core/tools/writeToFileTool.ts | 2 +- src/core/webview/ClineProvider.ts | 4 ++-- src/core/webview/webviewMessageHandler.ts | 7 +++++-- src/integrations/misc/open-file.ts | 4 +++- .../terminal/ExecaTerminalProcess.ts | 4 +++- .../checkpoints/ShadowCheckpointService.ts | 2 +- src/services/checkpoints/excludes.ts | 4 +++- src/services/code-index/config-manager.ts | 9 +++++++-- src/shared/api.ts | 13 +++++++++++-- src/shared/combineApiRequests.ts | 8 ++++++-- src/shared/modes.ts | 2 +- src/utils/tts.ts | 4 +++- .../src/components/chat/AutoApproveMenu.tsx | 2 +- webview-ui/src/components/chat/ChatView.tsx | 8 +++++--- webview-ui/src/components/chat/TaskActions.tsx | 2 +- webview-ui/src/components/chat/TaskHeader.tsx | 4 ++-- .../src/components/history/TaskItemFooter.tsx | 17 ++++++++++------- .../components/settings/ApiConfigManager.tsx | 5 ++++- .../components/settings/AutoApproveToggle.tsx | 2 +- .../settings/ExperimentalSettings.tsx | 5 ++++- .../src/components/settings/ThinkingBudget.tsx | 10 ++++++---- .../components/settings/providers/Anthropic.tsx | 4 +++- .../components/settings/providers/Bedrock.tsx | 4 ++-- .../components/settings/providers/Gemini.tsx | 2 +- .../components/settings/providers/OpenAI.tsx | 2 +- .../settings/providers/OpenAICompatible.tsx | 10 +++++++--- .../settings/providers/OpenRouter.tsx | 10 +++++++--- .../providers/__tests__/Bedrock.test.tsx | 16 ++++++++++++---- webview-ui/src/components/ui/chat/ChatInput.tsx | 2 +- .../src/components/ui/chat/ChatMessages.tsx | 4 +++- .../components/ui/hooks/useOpenRouterKeyInfo.ts | 2 +- .../components/ui/hooks/useRequestyKeyInfo.ts | 2 +- 38 files changed, 137 insertions(+), 64 deletions(-) diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index 81c0b45e41..3b95ce729a 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -174,6 +174,7 @@ describe("OpenAiHandler", () => { const stream = reasoningHandler.createMessage(systemPrompt, messages) // Consume the stream to trigger the API call for await (const _chunk of stream) { + console.debug("Consuming stream for reasoning test") } // Assert the mockCreate was called with reasoning_effort expect(mockCreate).toHaveBeenCalled() @@ -191,6 +192,7 @@ describe("OpenAiHandler", () => { const stream = noReasoningHandler.createMessage(systemPrompt, messages) // Consume the stream to trigger the API call for await (const _chunk of stream) { + console.debug("Consuming stream for no-reasoning test") } // Assert the mockCreate was called without reasoning_effort expect(mockCreate).toHaveBeenCalled() diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts index 6474371bee..f4b1d421ba 100644 --- a/src/api/providers/vscode-lm.ts +++ b/src/api/providers/vscode-lm.ts @@ -291,7 +291,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan if (!this.client) { console.debug("Roo Code : Getting client with options:", { vsCodeLmModelSelector: this.options.vsCodeLmModelSelector, - hasOptions: !!this.options, + hasOptions: true, // this.options is guaranteed to be an ApiHandlerOptions object selectorKeys: this.options.vsCodeLmModelSelector ? Object.keys(this.options.vsCodeLmModelSelector) : [], }) diff --git a/src/core/condense/index.ts b/src/core/condense/index.ts index 8a8b57bb0c..dc59a9d394 100644 --- a/src/core/condense/index.ts +++ b/src/core/condense/index.ts @@ -93,8 +93,8 @@ export async function summarizeConversation( TelemetryService.instance.captureContextCondensed( taskId, isAutomaticTrigger ?? false, - !!customCondensingPrompt?.trim(), - !!condensingApiHandler, + typeof customCondensingPrompt === "string" && customCondensingPrompt.trim() !== "", + condensingApiHandler !== undefined, ) const response: SummarizeResponse = { messages, cost: 0, summary: "" } diff --git a/src/core/config/ContextProxy.ts b/src/core/config/ContextProxy.ts index c4324fbb13..497002a81d 100644 --- a/src/core/config/ContextProxy.ts +++ b/src/core/config/ContextProxy.ts @@ -205,7 +205,7 @@ export class ContextProxy { await this.setValues({ ...PROVIDER_SETTINGS_KEYS.filter((key) => !isSecretStateKey(key)) - .filter((key) => !!this.stateCache[key]) + .filter((key) => this.stateCache[key] != null) .reduce((acc, key) => ({ ...acc, [key]: undefined }), {} as ProviderSettings), ...values, }) diff --git a/src/core/config/importExport.ts b/src/core/config/importExport.ts index 4830a5f987..ea2be6f0fc 100644 --- a/src/core/config/importExport.ts +++ b/src/core/config/importExport.ts @@ -117,5 +117,7 @@ export const exportSettings = async ({ providerSettingsManager, contextProxy }: const dirname = path.dirname(uri.fsPath) await fs.mkdir(dirname, { recursive: true }) await fs.writeFile(uri.fsPath, JSON.stringify({ providerProfiles, globalSettings }, null, 2), "utf-8") - } catch (e) {} + } catch (e) { + console.debug("Error exporting settings:", e) + } } diff --git a/src/core/tools/executeCommandTool.ts b/src/core/tools/executeCommandTool.ts index e38d3c74f6..41b0c3c164 100644 --- a/src/core/tools/executeCommandTool.ts +++ b/src/core/tools/executeCommandTool.ts @@ -171,7 +171,9 @@ export async function executeCommand( message = { text, images } process.continue() } - } catch (_error) {} + } catch (_error) { + console.debug("Error in onWillRespond:", _error) + } }, onCompleted: (output: string | undefined) => { result = Terminal.compressTerminalOutput(output ?? "", terminalOutputLineLimit) @@ -197,7 +199,12 @@ export async function executeCommand( } } - const terminal = await TerminalRegistry.getOrCreateTerminal(workingDir, !!customCwd, cline.taskId, terminalProvider) + const terminal = await TerminalRegistry.getOrCreateTerminal( + workingDir, + typeof customCwd === "string" && customCwd !== "", + cline.taskId, + terminalProvider, + ) if (terminal instanceof Terminal) { terminal.terminal.show(true) diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index 63191acb7e..83543919b7 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -123,7 +123,7 @@ export async function writeToFileTool( const isNewFile = !fileExists // Check if diffStrategy is enabled - const diffStrategyEnabled = !!cline.diffStrategy + const diffStrategyEnabled = cline.diffStrategy !== undefined // Use more specific error message for line_count that provides guidance based on the situation await cline.say( diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 172685affd..ccf26d41cd 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -825,7 +825,7 @@ export class ClineProvider } public hasProviderProfileEntry(name: string): boolean { - return !!this.getProviderProfileEntry(name) + return this.getProviderProfileEntry(name) !== undefined } async upsertProviderProfile( @@ -1656,7 +1656,7 @@ export class ClineProvider apiProvider: apiConfiguration?.apiProvider, modelId: task?.api?.getModel().id, diffStrategy: task?.diffStrategy?.getName(), - isSubtask: task ? !!task.parentTask : undefined, + isSubtask: task ? task.parentTask !== undefined : undefined, } } } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 659d60f31a..e4087708ac 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -629,7 +629,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We // Send the result back to the webview await provider.postMessageToWebview({ type: "browserConnectionResult", - success: !!chromeHostUrl, + success: typeof chromeHostUrl === "string" && chromeHostUrl !== "", text: `Auto-discovered and tested connection to Chrome: ${chromeHostUrl}`, values: { endpoint: chromeHostUrl }, }) @@ -1008,7 +1008,10 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We // Try to get enhancement config first, fall back to current config. let configToUse: ProviderSettings = apiConfiguration - if (enhancementApiConfigId && !!listApiConfigMeta.find(({ id }) => id === enhancementApiConfigId)) { + if ( + enhancementApiConfigId && + listApiConfigMeta.find(({ id }) => id === enhancementApiConfigId) !== undefined + ) { const { name: _, ...providerSettings } = await provider.providerSettingsManager.getProfile({ id: enhancementApiConfigId, }) diff --git a/src/integrations/misc/open-file.ts b/src/integrations/misc/open-file.ts index 9318e23766..c542738cb1 100644 --- a/src/integrations/misc/open-file.ts +++ b/src/integrations/misc/open-file.ts @@ -138,7 +138,9 @@ export async function openFile(filePath: string, options: OpenFileOptions = {}) break } } - } catch {} // not essential, sometimes tab operations fail + } catch (e) { + console.debug("Error processing tab operations:", e) + } // not essential, sometimes tab operations fail const document = await vscode.workspace.openTextDocument(uriToProcess) const selection = diff --git a/src/integrations/terminal/ExecaTerminalProcess.ts b/src/integrations/terminal/ExecaTerminalProcess.ts index 2b9b97d10a..1f9478e573 100644 --- a/src/integrations/terminal/ExecaTerminalProcess.ts +++ b/src/integrations/terminal/ExecaTerminalProcess.ts @@ -76,7 +76,9 @@ export class ExecaTerminalProcess extends BaseTerminalProcess { timeoutId = setTimeout(() => { try { subprocess.kill("SIGKILL") - } catch (e) {} + } catch (e) { + console.debug("Error killing subprocess:", e) + } resolve() }, 5_000) diff --git a/src/services/checkpoints/ShadowCheckpointService.ts b/src/services/checkpoints/ShadowCheckpointService.ts index 8ec82f77ec..c1b39379cc 100644 --- a/src/services/checkpoints/ShadowCheckpointService.ts +++ b/src/services/checkpoints/ShadowCheckpointService.ts @@ -36,7 +36,7 @@ export abstract class ShadowCheckpointService extends EventEmitter { } public get isInitialized() { - return !!this.git + return this.git !== undefined } constructor(taskId: string, checkpointsDir: string, workspaceDir: string, log: (message: string) => void) { diff --git a/src/services/checkpoints/excludes.ts b/src/services/checkpoints/excludes.ts index 52469fd620..4e49ebdb32 100644 --- a/src/services/checkpoints/excludes.ts +++ b/src/services/checkpoints/excludes.ts @@ -193,7 +193,9 @@ const getLfsPatterns = async (workspacePath: string) => { .filter((line) => line.includes("filter=lfs")) .map((line) => line.split(" ")[0].trim()) } - } catch (error) {} + } catch (error) { + console.debug("Error getting LFS tracked files:", error) + } return [] } diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 730f43e3c5..7b31aa983a 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -124,13 +124,18 @@ export class CodeIndexConfigManager { if (this.embedderProvider === "openai") { const openAiKey = this.openAiOptions?.openAiNativeApiKey const qdrantUrl = this.qdrantUrl - const isConfigured = !!(openAiKey && qdrantUrl) + const isConfigured = + typeof openAiKey === "string" && openAiKey !== "" && typeof qdrantUrl === "string" && qdrantUrl !== "" return isConfigured } else if (this.embedderProvider === "ollama") { // Ollama model ID has a default, so only base URL is strictly required for config const ollamaBaseUrl = this.ollamaOptions?.ollamaBaseUrl const qdrantUrl = this.qdrantUrl - const isConfigured = !!(ollamaBaseUrl && qdrantUrl) + const isConfigured = + typeof ollamaBaseUrl === "string" && + ollamaBaseUrl !== "" && + typeof qdrantUrl === "string" && + qdrantUrl !== "" return isConfigured } return false // Should not happen if embedderProvider is always set correctly diff --git a/src/shared/api.ts b/src/shared/api.ts index 8ad8828658..f6f05fc713 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -34,7 +34,9 @@ export const shouldUseReasoningBudget = ({ }: { model: ModelInfo settings?: ProviderSettings -}): boolean => !!model.requiredReasoningBudget || (!!model.supportsReasoningBudget && !!settings?.enableReasoningEffort) +}): boolean => + model.requiredReasoningBudget === true || + (model.supportsReasoningBudget === true && settings?.enableReasoningEffort === true) export const shouldUseReasoningEffort = ({ model, @@ -42,7 +44,14 @@ export const shouldUseReasoningEffort = ({ }: { model: ModelInfo settings?: ProviderSettings -}): boolean => (!!model.supportsReasoningEffort && !!settings?.reasoningEffort) || !!model.reasoningEffort +}): boolean => + (model.supportsReasoningEffort === true && + settings?.reasoningEffort !== undefined && + (typeof settings.reasoningEffort === "string" || // String literals are inherently non-empty + (typeof settings.reasoningEffort === "number" && settings.reasoningEffort !== 0))) || + (model.reasoningEffort !== undefined && + (typeof model.reasoningEffort === "string" || // String literals are inherently non-empty + (typeof model.reasoningEffort === "number" && model.reasoningEffort !== 0))) export const DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS = 16_384 export const DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS = 8_192 diff --git a/src/shared/combineApiRequests.ts b/src/shared/combineApiRequests.ts index 20ba6bb6aa..dbfc24ea77 100644 --- a/src/shared/combineApiRequests.ts +++ b/src/shared/combineApiRequests.ts @@ -68,13 +68,17 @@ export function combineApiRequests(messages: ClineMessage[]): ClineMessage[] { if (startMessage.text) { startData = JSON.parse(startMessage.text) } - } catch (e) {} + } catch (e) { + console.debug("Error parsing startMessage text:", e) + } try { if (message.text) { finishData = JSON.parse(message.text) } - } catch (e) {} + } catch (e) { + console.debug("Error parsing message text:", e) + } result[startIndex] = { ...startMessage, text: JSON.stringify({ ...startData, ...finishData }) } } diff --git a/src/shared/modes.ts b/src/shared/modes.ts index c735118f66..ad8946ecf7 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -154,7 +154,7 @@ export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] { // Check if a mode is custom or an override export function isCustomMode(slug: string, customModes?: ModeConfig[]): boolean { - return !!customModes?.some((mode) => mode.slug === slug) + return Array.isArray(customModes) && customModes.some((mode) => mode.slug === slug) } /** diff --git a/src/utils/tts.ts b/src/utils/tts.ts index b544960571..eba572ceb3 100644 --- a/src/utils/tts.ts +++ b/src/utils/tts.ts @@ -32,7 +32,9 @@ export const playTts = async (message: string, options: PlayTtsOptions = {}) => try { queue.push({ message, options }) await processQueue() - } catch (error) {} + } catch (error) { + console.debug("Error processing TTS queue:", error) + } } export const stopTts = () => { diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index ac02d6b8c4..f1907100e4 100644 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ b/webview-ui/src/components/chat/AutoApproveMenu.tsx @@ -108,7 +108,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { ) const enabledActionsList = Object.entries(toggles) - .filter(([_key, value]) => !!value) + .filter(([_key, value]) => value === true) .map(([key]) => t(autoApproveSettingsConfig[key as AutoApproveSetting].labelKey)) .join(", ") diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index d114701e18..f1c876d8bb 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -159,7 +159,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction !!apiConfiguration && !ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList), + () => + apiConfiguration !== undefined && + !ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList), [apiConfiguration, organizationAllowList], ) @@ -429,7 +431,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { disabled={buttonsDisabled} onClick={() => vscode.postMessage({ type: "exportCurrentTask" })} /> - {!!item?.size && item.size > 0 && ( + {typeof item?.size === "number" && item.size > 0 && ( <>
{condenseButton} - {!!totalCost && ${totalCost.toFixed(2)}} + {totalCost > 0 && ${totalCost.toFixed(2)}}
)} {/* Expanded state: Show task text and images */} @@ -204,7 +204,7 @@ const TaskHeader = ({ )} - {!!totalCost && ( + {totalCost > 0 && (
{t("chat:task.apiCost")} diff --git a/webview-ui/src/components/history/TaskItemFooter.tsx b/webview-ui/src/components/history/TaskItemFooter.tsx index b3e6e56371..8682f84fad 100644 --- a/webview-ui/src/components/history/TaskItemFooter.tsx +++ b/webview-ui/src/components/history/TaskItemFooter.tsx @@ -34,7 +34,7 @@ const TaskItemFooter: React.FC = ({ item, variant, isSelect {isCompact ? ( <> {/* Compact Cache */} - {!!item.cacheWrites && ( + {typeof item.cacheWrites === "number" && item.cacheWrites > 0 && ( {formatLargeNumber(item.cacheWrites || 0)} @@ -42,7 +42,7 @@ const TaskItemFooter: React.FC = ({ item, variant, isSelect {formatLargeNumber(item.cacheReads || 0)} )} - + {/* Compact Tokens */} {(item.tokensIn || item.tokensOut) && ( <> @@ -55,7 +55,7 @@ const TaskItemFooter: React.FC = ({ item, variant, isSelect )} {/* Compact Cost */} - {!!item.totalCost && ( + {item.totalCost > 0 && ( {"$" + item.totalCost.toFixed(2)} @@ -66,7 +66,7 @@ const TaskItemFooter: React.FC = ({ item, variant, isSelect <>
{/* Cache Info */} - {!!item.cacheWrites && ( + {typeof item.cacheWrites === "number" && item.cacheWrites > 0 && (
{t("history:cacheLabel")} @@ -74,12 +74,15 @@ const TaskItemFooter: React.FC = ({ item, variant, isSelect {formatLargeNumber(item.cacheWrites || 0)} - + {formatLargeNumber(item.cacheReads || 0)}
)} - + {/* Full Tokens */} {(item.tokensIn || item.tokensOut) && (
@@ -95,7 +98,7 @@ const TaskItemFooter: React.FC = ({ item, variant, isSelect
)} {/* Full Cost */} - {!!item.totalCost && ( + {item.totalCost > 0 && (
{t("history:apiCostLabel")} {"$" + item.totalCost.toFixed(4)} diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx index ddcfae49b3..7db5c767ab 100644 --- a/webview-ui/src/components/settings/ApiConfigManager.tsx +++ b/webview-ui/src/components/settings/ApiConfigManager.tsx @@ -72,7 +72,10 @@ const ApiConfigManager = ({ } // If provider allows all models, profile is valid - return !!providerConfig.allowAll || !!(providerConfig.models && providerConfig.models.length > 0) + return ( + providerConfig.allowAll === true || + (Array.isArray(providerConfig.models) && providerConfig.models.length > 0) + ) } const validateName = (name: string, isNewProfile: boolean): string | null => { diff --git a/webview-ui/src/components/settings/AutoApproveToggle.tsx b/webview-ui/src/components/settings/AutoApproveToggle.tsx index ffad47e2ac..27a41a623a 100644 --- a/webview-ui/src/components/settings/AutoApproveToggle.tsx +++ b/webview-ui/src/components/settings/AutoApproveToggle.tsx @@ -106,7 +106,7 @@ export const AutoApproveToggle = ({ onToggle, ...props }: AutoApproveToggleProps onClick={() => onToggle(key, !props[key])} title={t(descriptionKey || "")} aria-label={t(labelKey)} - aria-pressed={!!props[key]} + aria-pressed={props[key] === true} data-testid={testId} className={cn(" aspect-square h-[80px]", !props[key] && "opacity-50")}> diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx index 5525a842b2..d8bc6691d5 100644 --- a/webview-ui/src/components/settings/ExperimentalSettings.tsx +++ b/webview-ui/src/components/settings/ExperimentalSettings.tsx @@ -78,7 +78,10 @@ export const ExperimentalSettings = ({ experimentKey={config[0]} enabled={experiments[EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS]] ?? false} onChange={(enabled) => - setExperimentEnabled(EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS], enabled) + setExperimentEnabled( + EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS], + enabled, + ) } /> ) diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx index 456e0be17a..ddb66f1d29 100644 --- a/webview-ui/src/components/settings/ThinkingBudget.tsx +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -17,9 +17,9 @@ interface ThinkingBudgetProps { export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => { const { t } = useAppTranslation() - const isReasoningBudgetSupported = !!modelInfo && modelInfo.supportsReasoningBudget - const isReasoningBudgetRequired = !!modelInfo && modelInfo.requiredReasoningBudget - const isReasoningEffortSupported = !!modelInfo && modelInfo.supportsReasoningEffort + const isReasoningBudgetSupported = modelInfo !== undefined && modelInfo.supportsReasoningBudget === true + const isReasoningBudgetRequired = modelInfo !== undefined && modelInfo.requiredReasoningBudget === true + const isReasoningEffortSupported = modelInfo !== undefined && modelInfo.supportsReasoningEffort === true const enableReasoningEffort = apiConfiguration.enableReasoningEffort const customMaxOutputTokens = apiConfiguration.modelMaxTokens || DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS @@ -45,7 +45,9 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod return null } - return isReasoningBudgetSupported && !!modelInfo.maxTokens ? ( + return isReasoningBudgetSupported && + typeof modelInfo.maxTokens === "number" && // modelInfo is guaranteed to be defined here + modelInfo.maxTokens > 0 ? ( <> {!isReasoningBudgetRequired && (
diff --git a/webview-ui/src/components/settings/providers/Anthropic.tsx b/webview-ui/src/components/settings/providers/Anthropic.tsx index f340e73f72..9830b3b112 100644 --- a/webview-ui/src/components/settings/providers/Anthropic.tsx +++ b/webview-ui/src/components/settings/providers/Anthropic.tsx @@ -17,7 +17,9 @@ type AnthropicProps = { export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: AnthropicProps) => { const { t } = useAppTranslation() - const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) + const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState( + typeof apiConfiguration?.anthropicBaseUrl === "string" && apiConfiguration.anthropicBaseUrl !== "", + ) const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/Bedrock.tsx b/webview-ui/src/components/settings/providers/Bedrock.tsx index eb8ca94258..7fe124430d 100644 --- a/webview-ui/src/components/settings/providers/Bedrock.tsx +++ b/webview-ui/src/components/settings/providers/Bedrock.tsx @@ -17,11 +17,11 @@ type BedrockProps = { export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedModelInfo }: BedrockProps) => { const { t } = useAppTranslation() - const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpointEnabled) + const [awsEndpointSelected, setAwsEndpointSelected] = useState(apiConfiguration?.awsBedrockEndpointEnabled === true) // Update the endpoint enabled state when the configuration changes useEffect(() => { - setAwsEndpointSelected(!!apiConfiguration?.awsBedrockEndpointEnabled) + setAwsEndpointSelected(apiConfiguration?.awsBedrockEndpointEnabled === true) }, [apiConfiguration?.awsBedrockEndpointEnabled]) const handleInputChange = useCallback( diff --git a/webview-ui/src/components/settings/providers/Gemini.tsx b/webview-ui/src/components/settings/providers/Gemini.tsx index 21056f12d5..86ed562dca 100644 --- a/webview-ui/src/components/settings/providers/Gemini.tsx +++ b/webview-ui/src/components/settings/providers/Gemini.tsx @@ -18,7 +18,7 @@ export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiPro const { t } = useAppTranslation() const [googleGeminiBaseUrlSelected, setGoogleGeminiBaseUrlSelected] = useState( - !!apiConfiguration?.googleGeminiBaseUrl, + typeof apiConfiguration?.googleGeminiBaseUrl === "string" && apiConfiguration.googleGeminiBaseUrl !== "", ) const handleInputChange = useCallback( diff --git a/webview-ui/src/components/settings/providers/OpenAI.tsx b/webview-ui/src/components/settings/providers/OpenAI.tsx index e2f7857fe0..905adb71eb 100644 --- a/webview-ui/src/components/settings/providers/OpenAI.tsx +++ b/webview-ui/src/components/settings/providers/OpenAI.tsx @@ -18,7 +18,7 @@ export const OpenAI = ({ apiConfiguration, setApiConfigurationField }: OpenAIPro const { t } = useAppTranslation() const [openAiNativeBaseUrlSelected, setOpenAiNativeBaseUrlSelected] = useState( - !!apiConfiguration?.openAiNativeBaseUrl, + typeof apiConfiguration?.openAiNativeBaseUrl === "string" && apiConfiguration.openAiNativeBaseUrl !== "", ) const handleInputChange = useCallback( diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index 43fea540c3..a73a56abd6 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -36,8 +36,12 @@ export const OpenAICompatible = ({ }: OpenAICompatibleProps) => { const { t } = useAppTranslation() - const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion) - const [openAiLegacyFormatSelected, setOpenAiLegacyFormatSelected] = useState(!!apiConfiguration?.openAiLegacyFormat) + const [azureApiVersionSelected, setAzureApiVersionSelected] = useState( + typeof apiConfiguration?.azureApiVersion === "string" && apiConfiguration.azureApiVersion !== "", + ) + const [openAiLegacyFormatSelected, setOpenAiLegacyFormatSelected] = useState( + apiConfiguration?.openAiLegacyFormat === true, + ) const [openAiModels, setOpenAiModels] = useState | null>(null) @@ -244,7 +248,7 @@ export const OpenAICompatible = ({ }}> {t("settings:providers.setReasoningLevel")} - {!!apiConfiguration.enableReasoningEffort && ( + {apiConfiguration.enableReasoningEffort === true && ( { const { t } = useAppTranslation() - const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl) + const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState( + typeof apiConfiguration?.openRouterBaseUrl === "string" && apiConfiguration.openRouterBaseUrl !== "", + ) const handleInputChange = useCallback( ( @@ -58,8 +60,10 @@ export const OpenRouter = ({ const { data: openRouterModelProviders } = useOpenRouterModelProviders(apiConfiguration?.openRouterModelId, { enabled: - !!apiConfiguration?.openRouterModelId && - routerModels?.openrouter && + typeof apiConfiguration?.openRouterModelId === "string" && + apiConfiguration.openRouterModelId !== "" && + routerModels?.openrouter !== undefined && + routerModels?.openrouter !== null && Object.keys(routerModels.openrouter).length > 1 && apiConfiguration.openRouterModelId in routerModels.openrouter, }) diff --git a/webview-ui/src/components/settings/providers/__tests__/Bedrock.test.tsx b/webview-ui/src/components/settings/providers/__tests__/Bedrock.test.tsx index c1f177ac7b..556de4ded1 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Bedrock.test.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Bedrock.test.tsx @@ -353,7 +353,9 @@ describe("Bedrock Component", () => { ) // Verify checkbox is checked and endpoint is visible - expect(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint")).toBeChecked() + expect( + screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint"), + ).toBeChecked() expect(screen.getByTestId("vpc-endpoint-input")).toBeInTheDocument() expect(screen.getByTestId("vpc-endpoint-input")).toHaveValue("https://custom-endpoint.aws.com") @@ -374,7 +376,9 @@ describe("Bedrock Component", () => { ) // Verify checkbox is unchecked and endpoint is not visible - expect(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint")).not.toBeChecked() + expect( + screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint"), + ).not.toBeChecked() expect(screen.queryByTestId("vpc-endpoint-input")).not.toBeInTheDocument() }) @@ -394,7 +398,9 @@ describe("Bedrock Component", () => { ) // Verify initial state - expect(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint")).not.toBeChecked() + expect( + screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint"), + ).not.toBeChecked() expect(screen.queryByTestId("vpc-endpoint-input")).not.toBeInTheDocument() // Update with new configuration @@ -412,7 +418,9 @@ describe("Bedrock Component", () => { ) // Verify updated state - expect(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint")).toBeChecked() + expect( + screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint"), + ).toBeChecked() expect(screen.getByTestId("vpc-endpoint-input")).toBeInTheDocument() expect(screen.getByTestId("vpc-endpoint-input")).toHaveValue("https://updated-endpoint.aws.com") }) diff --git a/webview-ui/src/components/ui/chat/ChatInput.tsx b/webview-ui/src/components/ui/chat/ChatInput.tsx index 8bec35b842..ea98cda713 100644 --- a/webview-ui/src/components/ui/chat/ChatInput.tsx +++ b/webview-ui/src/components/ui/chat/ChatInput.tsx @@ -80,7 +80,7 @@ function ChatInputField({ placeholder = "Chat" }: ChatInputFieldProps) { function ChatInputSubmit() { const { isLoading, stop } = useChatUI() const { isDisabled } = useChatInput() - const isStoppable = isLoading && !!stop + const isStoppable = isLoading && typeof stop === "function" return (
diff --git a/webview-ui/src/components/ui/chat/ChatMessages.tsx b/webview-ui/src/components/ui/chat/ChatMessages.tsx index bcaffd2397..61d50e5ffa 100644 --- a/webview-ui/src/components/ui/chat/ChatMessages.tsx +++ b/webview-ui/src/components/ui/chat/ChatMessages.tsx @@ -29,7 +29,9 @@ export function ChatMessages() { key={index} message={message} isHeaderVisible={ - !!message.annotations?.length || index === 0 || messages[index - 1].role !== message.role + (Array.isArray(message.annotations) && message.annotations.length > 0) || + index === 0 || + messages[index - 1].role !== message.role } isLast={index === messageCount - 1} isLoading={isLoading} diff --git a/webview-ui/src/components/ui/hooks/useOpenRouterKeyInfo.ts b/webview-ui/src/components/ui/hooks/useOpenRouterKeyInfo.ts index 44b7aaade5..373d5392a6 100644 --- a/webview-ui/src/components/ui/hooks/useOpenRouterKeyInfo.ts +++ b/webview-ui/src/components/ui/hooks/useOpenRouterKeyInfo.ts @@ -53,7 +53,7 @@ export const useOpenRouterKeyInfo = (apiKey?: string, baseUrl?: string, options? queryKey: ["openrouter-key-info", apiKey, baseUrl], queryFn: () => getOpenRouterKeyInfo(apiKey, baseUrl), staleTime: 30 * 1000, // 30 seconds - enabled: !!apiKey, + enabled: typeof apiKey === "string" && apiKey !== "", ...options, }) } diff --git a/webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts b/webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts index e44aa5fcd1..014a694664 100644 --- a/webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts +++ b/webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts @@ -44,7 +44,7 @@ export const useRequestyKeyInfo = (apiKey?: string, options?: UseRequestyKeyInfo queryKey: ["requesty-key-info", apiKey], queryFn: () => getRequestyKeyInfo(apiKey), staleTime: 30 * 1000, // 30 seconds - enabled: !!apiKey, + enabled: typeof apiKey === "string" && apiKey !== "", ...options, }) } From f824807c05e0dfeec27bedc6ce09a4b1eff13825 Mon Sep 17 00:00:00 2001 From: Eric Wheeler Date: Mon, 2 Jun 2025 15:55:09 -0700 Subject: [PATCH 4/4] docs: update eslint-no-implicit-coercion rule Refine guidance on leveraging TypeScript types for explicit boolean checks. Clarify that redundant checks for conditions already guaranteed by type definitions (e.g., null checks for `SomeType \| undefined`, or undefined checks for non-optional members) should be omitted. Signed-off-by: Eric Wheeler --- .roo/rules/eslint-no-implicit-coercion.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.roo/rules/eslint-no-implicit-coercion.md b/.roo/rules/eslint-no-implicit-coercion.md index 9c88814f45..8ed6ae004b 100644 --- a/.roo/rules/eslint-no-implicit-coercion.md +++ b/.roo/rules/eslint-no-implicit-coercion.md @@ -6,6 +6,11 @@ When ESLint's `no-implicit-coercion` rule flags an error, a value is being impli For boolean coercions (e.g., in `if` statements or ternaries), MUST NOT use `Boolean(value)`. Instead, extend conditional expressions to explicitly compare against the specific data type and value expected for that implementation context. +- Leverage TypeScript type information to make these explicit comparisons precise. For example: + - Redundant checks for conditions already guaranteed by TypeScript type definitions (e.g., checking for `null` when a type is `SomeType | undefined`, or checking for `undefined` on a non-optional, initialized variable) MUST be omitted. + - If a string type is a union of specific non-empty literals (e.g., `"active" | "pending"`), `typeof variable === "string"` can be sufficient for its "truthiness" if all literals are inherently truthy. + - If types and initialization guarantee a variable is always defined and non-null (e.g., a non-optional class member), runtime checks for its mere existence can be redundant; the explicit boolean should reflect this guarantee. + YOU MUST NEVER replace `if (myVar)` with `if (Boolean(myVar))` or `if (!!myVar)` with `if (Boolean(myVar))`. This rule's purpose is to encourage a thoughtful evaluation of what "truthy" or "falsy" means for the specific variable and logic. @@ -75,5 +80,6 @@ Explicitly comparing types and values: 2. Reduces ambiguity regarding how falsy values (`null`, `undefined`, `0`, `""`, `NaN`) are handled. 3. Helps avoid bugs from overly general truthiness checks when specific conditions were needed. 4. Be careful of regular expression evaluations. For example, `/foo (.*)bar/` will legitimately match an empty string, or it may not match at all. You MUST differentiate between `match === undefined` vs `typeof match === 'string' && match != ""` because falsy evaluation MUST NOT BE USED because it is usually invalid and certainly imprecise. +5. Helps in distinguishing the original intent behind a `!!variable` check: was it for general "truthiness" or mere "existence (not undefined/null)"? This complements the type-aware checks mentioned in the Guideline. Always consider the context: What does "truthy" or "falsy" mean for this variable in this logic? Write conditions reflecting that precise meaning.