From d03c47e22223b8080b91bc7819406a6fcb1d3231 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Aug 2026 06:05:38 +0000 Subject: [PATCH 1/3] Initial plan From e49caf5985ef34163f3c9fe2c712deda5dc0d05e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Aug 2026 06:15:06 +0000 Subject: [PATCH 2/3] Fix unsafe variable declaration suggestions in try/catch rules Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...uire-fetch-response-body-try-catch.test.ts | 17 +++++ .../require-fetch-response-body-try-catch.ts | 60 ++++++++++------ .../require-json-parse-try-catch.test.ts | 15 ++++ .../src/rules/require-json-parse-try-catch.ts | 70 ++++++++++++------- 4 files changed, 118 insertions(+), 44 deletions(-) diff --git a/eslint-factory/src/rules/require-fetch-response-body-try-catch.test.ts b/eslint-factory/src/rules/require-fetch-response-body-try-catch.test.ts index 1c6b5a1df76..3fbe855b8d9 100644 --- a/eslint-factory/src/rules/require-fetch-response-body-try-catch.test.ts +++ b/eslint-factory/src/rules/require-fetch-response-body-try-catch.test.ts @@ -128,6 +128,23 @@ describe("require-fetch-response-body-try-catch", () => { }); }); + it("invalid: variable declaration used later is reported without suggestion (CommonJS)", () => { + cjsRuleTester.run("require-fetch-response-body-try-catch", requireFetchResponseBodyTryCatchRule, { + valid: [], + invalid: [ + { + code: `async function f() { + const response = await fetch(url); + const payload = await response.json(); + const pageArtifacts = Array.isArray(payload?.artifacts) ? payload.artifacts : []; + return pageArtifacts; + }`, + errors: [{ messageId: "requireTryCatch", suggestions: [] }], + }, + ], + }); + }); + it("invalid: variable resolved from bare await fetch, body read outside try is flagged (ES module)", () => { esmRuleTester.run("require-fetch-response-body-try-catch", requireFetchResponseBodyTryCatchRule, { valid: [], diff --git a/eslint-factory/src/rules/require-fetch-response-body-try-catch.ts b/eslint-factory/src/rules/require-fetch-response-body-try-catch.ts index 6910c299761..7cbee5548e1 100644 --- a/eslint-factory/src/rules/require-fetch-response-body-try-catch.ts +++ b/eslint-factory/src/rules/require-fetch-response-body-try-catch.ts @@ -83,6 +83,25 @@ export const requireFetchResponseBodyTryCatchRule = createRule({ return null; } + function canSuggestWrapStatement(stmt: TSESTree.Statement): boolean { + if (stmt.type !== AST_NODE_TYPES.VariableDeclaration || stmt.kind === "var") { + return true; + } + + const statementRange = stmt.range; + if (!statementRange) return false; + const [statementStart, statementEnd] = statementRange; + + const hasReferenceOutsideStatement = sourceCode.getDeclaredVariables(stmt).some(variable => + variable.references.some(reference => { + const referenceRange = reference.identifier.range; + return referenceRange == null || referenceRange[0] < statementStart || referenceRange[1] > statementEnd; + }) + ); + + return !hasReferenceOutsideStatement; + } + /** * Returns true when the identifier at `node` resolves (via any write reference in scope — * either its initializing declarator or a later reassignment) to a bare `await fetch(...)` @@ -131,27 +150,28 @@ export const requireFetchResponseBodyTryCatchRule = createRule({ node, messageId: "requireTryCatch", data: { call: callText }, - suggest: stmt - ? [ - { - messageId: "wrapInTryCatch", - fix(fixer) { - const stmtText = sourceCode.getText(stmt); - const startLine = stmt.loc?.start.line; - const stmtLine = startLine !== undefined ? (sourceCode.lines[startLine - 1] ?? "") : ""; - const indent = stmtLine.match(/^(\s*)/)?.[1] ?? ""; - return fixer.replaceText( - stmt, - buildTryCatchSuggestion(stmtText, { - indent, - todoComment: "TODO: handle a malformed/errored fetch response body for this call.", - errorPrefix: `Failed to read fetch response ${methodName}(): `, - }) - ); + suggest: + stmt && canSuggestWrapStatement(stmt) + ? [ + { + messageId: "wrapInTryCatch", + fix(fixer) { + const stmtText = sourceCode.getText(stmt); + const startLine = stmt.loc?.start.line; + const stmtLine = startLine !== undefined ? (sourceCode.lines[startLine - 1] ?? "") : ""; + const indent = stmtLine.match(/^(\s*)/)?.[1] ?? ""; + return fixer.replaceText( + stmt, + buildTryCatchSuggestion(stmtText, { + indent, + todoComment: "TODO: handle a malformed/errored fetch response body for this call.", + errorPrefix: `Failed to read fetch response ${methodName}(): `, + }) + ); + }, }, - }, - ] - : [], + ] + : [], }); }, }; diff --git a/eslint-factory/src/rules/require-json-parse-try-catch.test.ts b/eslint-factory/src/rules/require-json-parse-try-catch.test.ts index 7968dbd2360..44d69700ce0 100644 --- a/eslint-factory/src/rules/require-json-parse-try-catch.test.ts +++ b/eslint-factory/src/rules/require-json-parse-try-catch.test.ts @@ -114,6 +114,21 @@ describe("require-json-parse-try-catch", () => { }); }); + it("invalid: variable declaration used later is reported without suggestion", () => { + cjsRuleTester.run("require-json-parse-try-catch", requireJsonParseTryCatchRule, { + valid: [], + invalid: [ + { + code: `function parsePayload(rawInput) { + const payload = JSON.parse(rawInput); + return payload?.artifacts ?? []; + }`, + errors: [{ messageId: "requireTryCatch", data: { arg: "rawInput" }, suggestions: [] }], + }, + ], + }); + }); + it('invalid: computed JSON["parse"] access is flagged when not in try block', () => { cjsRuleTester.run("require-json-parse-try-catch", requireJsonParseTryCatchRule, { valid: [], diff --git a/eslint-factory/src/rules/require-json-parse-try-catch.ts b/eslint-factory/src/rules/require-json-parse-try-catch.ts index 890ba74fa0b..1ecce47339e 100644 --- a/eslint-factory/src/rules/require-json-parse-try-catch.ts +++ b/eslint-factory/src/rules/require-json-parse-try-catch.ts @@ -61,6 +61,25 @@ export const requireJsonParseTryCatchRule = createRule({ return null; } + function canSuggestWrapStatement(stmt: TSESTree.Statement): boolean { + if (stmt.type !== AST_NODE_TYPES.VariableDeclaration || stmt.kind === "var") { + return true; + } + + const statementRange = stmt.range; + if (!statementRange) return false; + const [statementStart, statementEnd] = statementRange; + + const hasReferenceOutsideStatement = sourceCode.getDeclaredVariables(stmt).some(variable => + variable.references.some(reference => { + const referenceRange = reference.identifier.range; + return referenceRange == null || referenceRange[0] < statementStart || referenceRange[1] > statementEnd; + }) + ); + + return !hasReferenceOutsideStatement; + } + return { CallExpression(node) { if (node.callee.type !== "MemberExpression") { @@ -90,34 +109,37 @@ export const requireJsonParseTryCatchRule = createRule({ if (!isInsideTryBlock(node)) { const argText = node.arguments.length > 0 ? sourceCode.getText(node.arguments[0]) : ""; + const stmt = findEnclosingStatement(node); + context.report({ node, messageId: "requireTryCatch", data: { arg: argText }, - suggest: [ - { - messageId: "useHelper", - fix(fixer) { - const stmt = findEnclosingStatement(node); - if (!stmt) return null; - const stmtText = sourceCode.getText(stmt); - // ESLint always sets loc on parsed nodes; the optional chain guards - // against hypothetical missing loc. loc.start.line is 1-based, so - // subtract 1 for the 0-based lines array index. - const startLine = stmt.loc?.start.line; - const stmtLine = startLine !== undefined ? (sourceCode.lines[startLine - 1] ?? "") : ""; - const indent = stmtLine.match(/^(\s*)/)?.[1] ?? ""; - return fixer.replaceText( - stmt, - buildTryCatchSuggestion(stmtText, { - indent, - todoComment: "TODO: handle parse failure for this code path.", - errorPrefix: "Failed to parse JSON: ", - }) - ); - }, - }, - ], + suggest: + stmt && canSuggestWrapStatement(stmt) + ? [ + { + messageId: "useHelper", + fix(fixer) { + const stmtText = sourceCode.getText(stmt); + // ESLint always sets loc on parsed nodes; the optional chain guards + // against hypothetical missing loc. loc.start.line is 1-based, so + // subtract 1 for the 0-based lines array index. + const startLine = stmt.loc?.start.line; + const stmtLine = startLine !== undefined ? (sourceCode.lines[startLine - 1] ?? "") : ""; + const indent = stmtLine.match(/^(\s*)/)?.[1] ?? ""; + return fixer.replaceText( + stmt, + buildTryCatchSuggestion(stmtText, { + indent, + todoComment: "TODO: handle parse failure for this code path.", + errorPrefix: "Failed to parse JSON: ", + }) + ); + }, + }, + ] + : [], }); } }, From d64588a5993c373a1633a268c1122196ceb1181d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:51:13 +0000 Subject: [PATCH 3/3] fix: suppress unsafe try/catch suggestions for non-standalone declarations Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- ...uire-fetch-response-body-try-catch.test.ts | 24 +++++++++++++++++++ .../require-fetch-response-body-try-catch.ts | 16 ++++++++++++- .../require-json-parse-try-catch.test.ts | 24 +++++++++++++++++++ .../src/rules/require-json-parse-try-catch.ts | 16 ++++++++++++- 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/eslint-factory/src/rules/require-fetch-response-body-try-catch.test.ts b/eslint-factory/src/rules/require-fetch-response-body-try-catch.test.ts index 3fbe855b8d9..767a86ef3ad 100644 --- a/eslint-factory/src/rules/require-fetch-response-body-try-catch.test.ts +++ b/eslint-factory/src/rules/require-fetch-response-body-try-catch.test.ts @@ -145,6 +145,30 @@ describe("require-fetch-response-body-try-catch", () => { }); }); + it("invalid: export declaration is reported without suggestion (ES module)", () => { + esmRuleTester.run("require-fetch-response-body-try-catch", requireFetchResponseBodyTryCatchRule, { + valid: [], + invalid: [ + { + code: `export const payload = await fetch(url).json();`, + errors: [{ messageId: "requireTryCatch", suggestions: [] }], + }, + ], + }); + }); + + it("invalid: for-loop initializer declaration is reported without suggestion (CommonJS)", () => { + cjsRuleTester.run("require-fetch-response-body-try-catch", requireFetchResponseBodyTryCatchRule, { + valid: [], + invalid: [ + { + code: `async function f() { for (let payload = await fetch(url).json(); payload; payload = null) {} }`, + errors: [{ messageId: "requireTryCatch", suggestions: [] }], + }, + ], + }); + }); + it("invalid: variable resolved from bare await fetch, body read outside try is flagged (ES module)", () => { esmRuleTester.run("require-fetch-response-body-try-catch", requireFetchResponseBodyTryCatchRule, { valid: [], diff --git a/eslint-factory/src/rules/require-fetch-response-body-try-catch.ts b/eslint-factory/src/rules/require-fetch-response-body-try-catch.ts index 7cbee5548e1..8b2009e02c6 100644 --- a/eslint-factory/src/rules/require-fetch-response-body-try-catch.ts +++ b/eslint-factory/src/rules/require-fetch-response-body-try-catch.ts @@ -84,7 +84,21 @@ export const requireFetchResponseBodyTryCatchRule = createRule({ } function canSuggestWrapStatement(stmt: TSESTree.Statement): boolean { - if (stmt.type !== AST_NODE_TYPES.VariableDeclaration || stmt.kind === "var") { + if (stmt.type !== AST_NODE_TYPES.VariableDeclaration) { + return true; + } + + const parent = stmt.parent; + const isStandaloneVariableDeclaration = + parent != null && + ((parent.type === AST_NODE_TYPES.Program && parent.body.includes(stmt)) || + (parent.type === AST_NODE_TYPES.BlockStatement && parent.body.includes(stmt)) || + (parent.type === AST_NODE_TYPES.SwitchCase && parent.consequent.includes(stmt))); + if (!isStandaloneVariableDeclaration) { + return false; + } + + if (stmt.kind === "var") { return true; } diff --git a/eslint-factory/src/rules/require-json-parse-try-catch.test.ts b/eslint-factory/src/rules/require-json-parse-try-catch.test.ts index 44d69700ce0..a9402a68d23 100644 --- a/eslint-factory/src/rules/require-json-parse-try-catch.test.ts +++ b/eslint-factory/src/rules/require-json-parse-try-catch.test.ts @@ -129,6 +129,30 @@ describe("require-json-parse-try-catch", () => { }); }); + it("invalid: export declaration is reported without suggestion", () => { + esmRuleTester.run("require-json-parse-try-catch", requireJsonParseTryCatchRule, { + valid: [], + invalid: [ + { + code: `export const payload = JSON.parse(rawInput);`, + errors: [{ messageId: "requireTryCatch", data: { arg: "rawInput" }, suggestions: [] }], + }, + ], + }); + }); + + it("invalid: for-loop initializer declaration is reported without suggestion", () => { + cjsRuleTester.run("require-json-parse-try-catch", requireJsonParseTryCatchRule, { + valid: [], + invalid: [ + { + code: `for (let payload = JSON.parse(rawInput); payload; payload = null) {}`, + errors: [{ messageId: "requireTryCatch", data: { arg: "rawInput" }, suggestions: [] }], + }, + ], + }); + }); + it('invalid: computed JSON["parse"] access is flagged when not in try block', () => { cjsRuleTester.run("require-json-parse-try-catch", requireJsonParseTryCatchRule, { valid: [], diff --git a/eslint-factory/src/rules/require-json-parse-try-catch.ts b/eslint-factory/src/rules/require-json-parse-try-catch.ts index 1ecce47339e..dfdae6aa1d8 100644 --- a/eslint-factory/src/rules/require-json-parse-try-catch.ts +++ b/eslint-factory/src/rules/require-json-parse-try-catch.ts @@ -62,7 +62,21 @@ export const requireJsonParseTryCatchRule = createRule({ } function canSuggestWrapStatement(stmt: TSESTree.Statement): boolean { - if (stmt.type !== AST_NODE_TYPES.VariableDeclaration || stmt.kind === "var") { + if (stmt.type !== AST_NODE_TYPES.VariableDeclaration) { + return true; + } + + const parent = stmt.parent; + const isStandaloneVariableDeclaration = + parent != null && + ((parent.type === AST_NODE_TYPES.Program && parent.body.includes(stmt)) || + (parent.type === AST_NODE_TYPES.BlockStatement && parent.body.includes(stmt)) || + (parent.type === AST_NODE_TYPES.SwitchCase && parent.consequent.includes(stmt))); + if (!isStandaloneVariableDeclaration) { + return false; + } + + if (stmt.kind === "var") { return true; }