Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,47 @@ 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: 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: [],
Expand Down
74 changes: 54 additions & 20 deletions eslint-factory/src/rules/require-fetch-response-body-try-catch.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,6 +83,39 @@ export const requireFetchResponseBodyTryCatchRule = createRule({
return null;
}

function canSuggestWrapStatement(stmt: TSESTree.Statement): boolean {
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;
}

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(...)`
Expand DownExpand Up@@ -131,27 +164,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}(): `,
})
);
},
},
},
]
: [],
]
: [],
});
},
};
Expand Down
39 changes: 39 additions & 0 deletions eslint-factory/src/rules/require-json-parse-try-catch.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,6 +114,45 @@ 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: 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: [],
Expand Down
84 changes: 60 additions & 24 deletions eslint-factory/src/rules/require-json-parse-try-catch.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,39 @@ export const requireJsonParseTryCatchRule = createRule({
return null;
}

function canSuggestWrapStatement(stmt: TSESTree.Statement): boolean {
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;
}

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") {
Expand DownExpand Up@@ -90,34 +123,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: ",
})
);
},
},
]
: [],
});
}
},
Expand Down