From 09cef2eae7db1444a832c5e213fa558ed0c15b2c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:54:50 +0000 Subject: [PATCH 1/4] Initial plan From 676847ecc6d4d7af6cc3dffa3632024cff645184 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:59:46 +0000 Subject: [PATCH 2/4] Plan guard tightening for null-safe typeof checks Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/pr-triage-agent.lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-triage-agent.lock.yml b/.github/workflows/pr-triage-agent.lock.yml index f03d2293e1e..8776ef0d21b 100644 --- a/.github/workflows/pr-triage-agent.lock.yml +++ b/.github/workflows/pr-triage-agent.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"13b159b3008ccce2d9c7233e7bfa0c5eea0583c57bd721b9ee9a33efa43bae5f","body_hash":"8d505062b276bc9c6fda2b79316117cb30c88ae04cab333ef78d113bf91c6aec","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.65","copilot-sdk":"1.0.4"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.15"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.15"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.15"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.15"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.32","digest":"sha256:63e46b56dfd70895a701b6fc6dd0189e11e2d875f327f1781e81b31848735477","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.32@sha256:63e46b56dfd70895a701b6fc6dd0189e11e2d875f327f1781e81b31848735477"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.16"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.16"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.16"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.16"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.32","digest":"sha256:63e46b56dfd70895a701b6fc6dd0189e11e2d875f327f1781e81b31848735477","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.32@sha256:63e46b56dfd70895a701b6fc6dd0189e11e2d875f327f1781e81b31848735477"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ From 5522de3be3a05ce213cbf91012fad7f0b6ba35fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:06:18 +0000 Subject: [PATCH 3/4] Tighten unsafe typeof object guards for catch rules Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../no-unsafe-catch-error-property.test.ts | 52 +++++++++- .../rules/no-unsafe-catch-error-property.ts | 95 ++++++++++++++---- ...nsafe-promise-catch-error-property.test.ts | 26 ++++- .../no-unsafe-promise-catch-error-property.ts | 97 ++++++++++++++----- 4 files changed, 220 insertions(+), 50 deletions(-) diff --git a/eslint-factory/src/rules/no-unsafe-catch-error-property.test.ts b/eslint-factory/src/rules/no-unsafe-catch-error-property.test.ts index 6f599c5dca1..72c9eae1ba4 100644 --- a/eslint-factory/src/rules/no-unsafe-catch-error-property.test.ts +++ b/eslint-factory/src/rules/no-unsafe-catch-error-property.test.ts @@ -342,17 +342,59 @@ try { }); }); - it("valid: typeof err === 'object' guard suppresses all warnings in the catch block", () => { + it("valid: typeof err === 'object' with non-null guard suppresses warnings in the catch block", () => { cjsRuleTester.run("no-unsafe-catch-error-property", noUnsafeCatchErrorPropertyRule, { valid: [ - `try { f(); } catch (err) { if (typeof err === 'object') { console.log(err.status); } }`, `try { f(); } catch (err) { if (typeof err === 'object' && err !== null) { console.log(err.status); } }`, - `try { f(); } catch (err) { if ('object' === typeof err) { console.log(err.status); } }`, + `try { f(); } catch (err) { if ('object' === typeof err && null !== err) { console.log(err.status); } }`, + `try { f(); } catch (err) { if (typeof err === 'object' && err != null) { console.log(err.status); } }`, + `try { f(); } catch (err) { if (err && typeof err === 'object') { console.log(err.status); } }`, + `try { f(); } catch (err) { if (!err) return; if (typeof err === 'object') { console.log(err.status); } }`, ], invalid: [], }); }); + it("invalid: bare typeof err === 'object' guard is insufficient", () => { + cjsRuleTester.run("no-unsafe-catch-error-property", noUnsafeCatchErrorPropertyRule, { + valid: [], + invalid: [ + { + code: `try { f(); } catch (err) { if (typeof err === 'object') { console.log(err.status); } }`, + errors: [ + { + messageId: "unsafeProperty", + data: { prop: "status", errorVar: "err" }, + suggestions: [ + { + messageId: "wrapWithInstanceof", + data: { errorVar: "err", prop: "status" }, + output: `try { f(); } catch (err) { if (typeof err === 'object') { console.log((err instanceof Error ? err.status : undefined)); } }`, + }, + ], + }, + ], + }, + { + code: `try { f(); } catch (err) { if ('object' === typeof err) { console.log(err.status); } }`, + errors: [ + { + messageId: "unsafeProperty", + data: { prop: "status", errorVar: "err" }, + suggestions: [ + { + messageId: "wrapWithInstanceof", + data: { errorVar: "err", prop: "status" }, + output: `try { f(); } catch (err) { if ('object' === typeof err) { console.log((err instanceof Error ? err.status : undefined)); } }`, + }, + ], + }, + ], + }, + ], + }); + }); + it("invalid: err.status without guard is flagged", () => { cjsRuleTester.run("no-unsafe-catch-error-property", noUnsafeCatchErrorPropertyRule, { valid: [], @@ -425,9 +467,9 @@ try { }); }); - it("valid: typeof err === 'object' guard suppresses .status access (mirrors real call sites)", () => { + it("valid: typeof err === 'object' with truthy err guard suppresses .status access", () => { cjsRuleTester.run("no-unsafe-catch-error-property", noUnsafeCatchErrorPropertyRule, { - valid: [`try { f(); } catch (err) { if (typeof err === 'object' && err.status === 404) { } }`], + valid: [`try { f(); } catch (err) { if (err && typeof err === 'object' && err.status === 404) { } }`], invalid: [], }); }); diff --git a/eslint-factory/src/rules/no-unsafe-catch-error-property.ts b/eslint-factory/src/rules/no-unsafe-catch-error-property.ts index ef389728788..0c880f29313 100644 --- a/eslint-factory/src/rules/no-unsafe-catch-error-property.ts +++ b/eslint-factory/src/rules/no-unsafe-catch-error-property.ts @@ -7,9 +7,34 @@ const UNSAFE_PROPERTIES = new Set(["message", "stack", "code", "status", "cause" interface CatchFrame { varName: string; hasGuard: boolean; + hasNonNullGuard: boolean; unsafeNodes: Array<{ node: TSESTree.MemberExpression; prop: string }>; } +function isTypeofObjectCheck(node: TSESTree.Expression, varName: string): boolean { + if (node.type !== AST_NODE_TYPES.BinaryExpression || node.operator !== "===") return false; + const { left, right } = node; + return ( + (left.type === AST_NODE_TYPES.UnaryExpression && left.operator === "typeof" && left.argument.type === AST_NODE_TYPES.Identifier && left.argument.name === varName && right.type === AST_NODE_TYPES.Literal && right.value === "object") || + (right.type === AST_NODE_TYPES.UnaryExpression && right.operator === "typeof" && right.argument.type === AST_NODE_TYPES.Identifier && right.argument.name === varName && left.type === AST_NODE_TYPES.Literal && left.value === "object") + ); +} + +function isNonNullGuardCheck(node: TSESTree.Expression, varName: string): boolean { + if (node.type === AST_NODE_TYPES.Identifier) { + return node.name === varName; + } + + if (node.type !== AST_NODE_TYPES.BinaryExpression || (node.operator !== "!==" && node.operator !== "!=")) { + return false; + } + + const isVarRef = (value: TSESTree.Expression): value is TSESTree.Identifier => value.type === AST_NODE_TYPES.Identifier && value.name === varName; + const isNullLiteral = (value: TSESTree.Expression): value is TSESTree.Literal => value.type === AST_NODE_TYPES.Literal && value.value === null; + + return (isVarRef(node.left) && isNullLiteral(node.right)) || (isVarRef(node.right) && isNullLiteral(node.left)); +} + export const noUnsafeCatchErrorPropertyRule = createRule({ name: "no-unsafe-catch-error-property", meta: { @@ -36,11 +61,11 @@ export const noUnsafeCatchErrorPropertyRule = createRule({ // Only handle simple identifier bindings; skip bare catch {} and destructuring patterns. // Push a sentinel frame so CatchClause:exit always has a matching pop. if (!param || param.type !== AST_NODE_TYPES.Identifier) { - catchStack.push({ varName: "", hasGuard: true, unsafeNodes: [] }); + catchStack.push({ varName: "", hasGuard: true, hasNonNullGuard: true, unsafeNodes: [] }); return; } - catchStack.push({ varName: param.name, hasGuard: false, unsafeNodes: [] }); + catchStack.push({ varName: param.name, hasGuard: false, hasNonNullGuard: false, unsafeNodes: [] }); }, "CatchClause:exit"() { @@ -90,7 +115,7 @@ export const noUnsafeCatchErrorPropertyRule = createRule({ }, // Detect catchVar instanceof Error — also accepted as a safe guard - // Detect typeof catchVar === 'object' — also accepted as a safe guard + // Detect typeof catchVar === 'object' with a non-null companion guard BinaryExpression(node) { if (catchStack.length === 0) return; const top = catchStack[catchStack.length - 1]; @@ -101,25 +126,53 @@ export const noUnsafeCatchErrorPropertyRule = createRule({ return; } - // typeof varName === 'object' or 'object' === typeof varName - if (node.operator === "===") { - const { left, right } = node; - const isTypeofObject = - (left.type === AST_NODE_TYPES.UnaryExpression && - left.operator === "typeof" && - left.argument.type === AST_NODE_TYPES.Identifier && - left.argument.name === top.varName && - right.type === AST_NODE_TYPES.Literal && - right.value === "object") || - (right.type === AST_NODE_TYPES.UnaryExpression && - right.operator === "typeof" && - right.argument.type === AST_NODE_TYPES.Identifier && - right.argument.name === top.varName && - left.type === AST_NODE_TYPES.Literal && - left.value === "object"); - if (isTypeofObject) { - top.hasGuard = true; + if (isNonNullGuardCheck(node, top.varName)) { + top.hasNonNullGuard = true; + return; + } + + if (isTypeofObjectCheck(node, top.varName) && top.hasNonNullGuard) { + top.hasGuard = true; + } + }, + + LogicalExpression(node) { + if (catchStack.length === 0) return; + const top = catchStack[catchStack.length - 1]; + if (!top || top.hasGuard || !top.varName || node.operator !== "&&") return; + + const conjuncts: TSESTree.Expression[] = []; + const collectConjuncts = (expr: TSESTree.Expression): void => { + if (expr.type === AST_NODE_TYPES.LogicalExpression && expr.operator === "&&") { + collectConjuncts(expr.left); + collectConjuncts(expr.right); + return; } + conjuncts.push(expr); + }; + collectConjuncts(node); + + const hasTypeofObject = conjuncts.some(expr => isTypeofObjectCheck(expr, top.varName)); + const hasNonNullGuard = conjuncts.some(expr => isNonNullGuardCheck(expr, top.varName)); + if (hasTypeofObject && hasNonNullGuard) { + top.hasGuard = true; + top.hasNonNullGuard = true; + } + }, + + IfStatement(node) { + if (catchStack.length === 0) return; + const top = catchStack[catchStack.length - 1]; + if (!top || top.hasGuard || !top.varName) return; + + if ( + node.test.type === AST_NODE_TYPES.UnaryExpression && + node.test.operator === "!" && + node.test.argument.type === AST_NODE_TYPES.Identifier && + node.test.argument.name === top.varName && + node.consequent.type === AST_NODE_TYPES.ReturnStatement + ) { + top.hasNonNullGuard = true; } }, diff --git a/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.test.ts b/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.test.ts index e990948a3f3..b5e8746ef4f 100644 --- a/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.test.ts +++ b/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.test.ts @@ -240,13 +240,35 @@ describe("no-unsafe-promise-catch-error-property", () => { }); }); - it("valid: typeof err === 'object' guard suppresses warnings in .catch() callback", () => { + it("valid: typeof err === 'object' with non-null guard suppresses warnings in .catch() callback", () => { cjsRuleTester.run("no-unsafe-promise-catch-error-property", noUnsafePromiseCatchErrorPropertyRule, { - valid: [`promise.catch(err => { if (typeof err === 'object') { console.log(err.status); } });`, `promise.catch(err => { if ('object' === typeof err) { console.log(err.status); } });`], + valid: [ + `promise.catch(err => { if (typeof err === 'object' && err !== null) { console.log(err.status); } });`, + `promise.catch(err => { if ('object' === typeof err && null !== err) { console.log(err.status); } });`, + `promise.catch(err => { if (typeof err === 'object' && err != null) { console.log(err.status); } });`, + `promise.catch(err => { if (err && typeof err === 'object') { console.log(err.status); } });`, + `promise.catch(err => { if (!err) return; if (typeof err === 'object') { console.log(err.status); } });`, + ], invalid: [], }); }); + it("invalid: bare typeof err === 'object' guard is insufficient in .catch() callback", () => { + cjsRuleTester.run("no-unsafe-promise-catch-error-property", noUnsafePromiseCatchErrorPropertyRule, { + valid: [], + invalid: [ + { + code: `promise.catch(err => { if (typeof err === 'object') { console.log(err.status); } });`, + errors: [{ messageId: "unsafeProperty", data: { prop: "status", errorVar: "err" } }], + }, + { + code: `promise.catch(err => { if ('object' === typeof err) { console.log(err.status); } });`, + errors: [{ messageId: "unsafeProperty", data: { prop: "status", errorVar: "err" } }], + }, + ], + }); + }); + it("invalid: err.status without guard is flagged in .catch() callback", () => { cjsRuleTester.run("no-unsafe-promise-catch-error-property", noUnsafePromiseCatchErrorPropertyRule, { valid: [], diff --git a/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.ts b/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.ts index 2114e612052..0ba4f8c4da4 100644 --- a/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.ts +++ b/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.ts @@ -7,9 +7,34 @@ const UNSAFE_PROPERTIES = new Set(["message", "stack", "code", "status", "cause" interface CatchFrame { varName: string; hasGuard: boolean; + hasNonNullGuard: boolean; unsafeNodes: Array<{ node: TSESTree.MemberExpression; prop: string }>; } +function isTypeofObjectCheck(node: TSESTree.Expression, varName: string): boolean { + if (node.type !== AST_NODE_TYPES.BinaryExpression || node.operator !== "===") return false; + const { left, right } = node; + return ( + (left.type === AST_NODE_TYPES.UnaryExpression && left.operator === "typeof" && left.argument.type === AST_NODE_TYPES.Identifier && left.argument.name === varName && right.type === AST_NODE_TYPES.Literal && right.value === "object") || + (right.type === AST_NODE_TYPES.UnaryExpression && right.operator === "typeof" && right.argument.type === AST_NODE_TYPES.Identifier && right.argument.name === varName && left.type === AST_NODE_TYPES.Literal && left.value === "object") + ); +} + +function isNonNullGuardCheck(node: TSESTree.Expression, varName: string): boolean { + if (node.type === AST_NODE_TYPES.Identifier) { + return node.name === varName; + } + + if (node.type !== AST_NODE_TYPES.BinaryExpression || (node.operator !== "!==" && node.operator !== "!=")) { + return false; + } + + const isVarRef = (value: TSESTree.Expression): value is TSESTree.Identifier => value.type === AST_NODE_TYPES.Identifier && value.name === varName; + const isNullLiteral = (value: TSESTree.Expression): value is TSESTree.Literal => value.type === AST_NODE_TYPES.Literal && value.value === null; + + return (isVarRef(node.left) && isNullLiteral(node.right)) || (isVarRef(node.right) && isNullLiteral(node.left)); +} + function isCatchCallback(node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression): boolean { const parent = node.parent; if (!parent || parent.type !== AST_NODE_TYPES.CallExpression) return false; @@ -45,14 +70,14 @@ export const noUnsafePromiseCatchErrorPropertyRule = createRule({ const params = node.params; // Only handle simple identifier bindings; skip no-param and destructuring callbacks. if (params.length === 1 && params[0].type === AST_NODE_TYPES.Identifier) { - stack.push({ varName: params[0].name, hasGuard: false, unsafeNodes: [] }); + stack.push({ varName: params[0].name, hasGuard: false, hasNonNullGuard: false, unsafeNodes: [] }); } else { - stack.push({ varName: "", hasGuard: true, unsafeNodes: [] }); + stack.push({ varName: "", hasGuard: true, hasNonNullGuard: true, unsafeNodes: [] }); } } else { // Sentinel: prevents the outer .catch() frame from collecting accesses // to a shadowed parameter name inside this nested function. - stack.push({ varName: "", hasGuard: true, unsafeNodes: [] }); + stack.push({ varName: "", hasGuard: true, hasNonNullGuard: true, unsafeNodes: [] }); } } @@ -101,7 +126,7 @@ export const noUnsafePromiseCatchErrorPropertyRule = createRule({ }, // Detect catchVar instanceof Error — also accepted as a safe guard - // Detect typeof catchVar === 'object' — also accepted as a safe guard + // Detect typeof catchVar === 'object' with a non-null companion guard BinaryExpression(node) { if (stack.length === 0) return; const top = stack[stack.length - 1]; @@ -112,25 +137,53 @@ export const noUnsafePromiseCatchErrorPropertyRule = createRule({ return; } - // typeof varName === 'object' or 'object' === typeof varName - if (node.operator === "===") { - const { left, right } = node; - const isTypeofObject = - (left.type === AST_NODE_TYPES.UnaryExpression && - left.operator === "typeof" && - left.argument.type === AST_NODE_TYPES.Identifier && - left.argument.name === top.varName && - right.type === AST_NODE_TYPES.Literal && - right.value === "object") || - (right.type === AST_NODE_TYPES.UnaryExpression && - right.operator === "typeof" && - right.argument.type === AST_NODE_TYPES.Identifier && - right.argument.name === top.varName && - left.type === AST_NODE_TYPES.Literal && - left.value === "object"); - if (isTypeofObject) { - top.hasGuard = true; + if (isNonNullGuardCheck(node, top.varName)) { + top.hasNonNullGuard = true; + return; + } + + if (isTypeofObjectCheck(node, top.varName) && top.hasNonNullGuard) { + top.hasGuard = true; + } + }, + + LogicalExpression(node) { + if (stack.length === 0) return; + const top = stack[stack.length - 1]; + if (!top || top.hasGuard || !top.varName || node.operator !== "&&") return; + + const conjuncts: TSESTree.Expression[] = []; + const collectConjuncts = (expr: TSESTree.Expression): void => { + if (expr.type === AST_NODE_TYPES.LogicalExpression && expr.operator === "&&") { + collectConjuncts(expr.left); + collectConjuncts(expr.right); + return; } + conjuncts.push(expr); + }; + collectConjuncts(node); + + const hasTypeofObject = conjuncts.some(expr => isTypeofObjectCheck(expr, top.varName)); + const hasNonNullGuard = conjuncts.some(expr => isNonNullGuardCheck(expr, top.varName)); + if (hasTypeofObject && hasNonNullGuard) { + top.hasGuard = true; + top.hasNonNullGuard = true; + } + }, + + IfStatement(node) { + if (stack.length === 0) return; + const top = stack[stack.length - 1]; + if (!top || top.hasGuard || !top.varName) return; + + if ( + node.test.type === AST_NODE_TYPES.UnaryExpression && + node.test.operator === "!" && + node.test.argument.type === AST_NODE_TYPES.Identifier && + node.test.argument.name === top.varName && + node.consequent.type === AST_NODE_TYPES.ReturnStatement + ) { + top.hasNonNullGuard = true; } }, From f9bf045bdbc573f2a9b8db2d2cfae010fc244989 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 02:33:07 +0000 Subject: [PATCH 4/4] Fix: remove standalone non-null guard from BinaryExpression, revert lockfile bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop the standalone `isNonNullGuardCheck` path from BinaryExpression in both no-unsafe-catch-error-property and no-unsafe-promise-catch-error-property. A bare `err !== null` in a separate if-block (without return) was incorrectly setting hasNonNullGuard=true as a persistent flag, making a later standalone `typeof err === 'object'` suppress diagnostics even though the two checks are in unrelated control-flow branches. Valid patterns (same-&& expression via LogicalExpression, or !err early-return via IfStatement) are unaffected. - Add test cases for the false-safe separated-guard pattern in both rule test suites. - Revert unrelated pr-triage-agent.lock.yml firewall bump (0.27.15→0.27.16). Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/pr-triage-agent.lock.yml | 2 +- .../no-unsafe-catch-error-property.test.ts | 17 +++++++++++++++++ .../src/rules/no-unsafe-catch-error-property.ts | 5 ----- ...-unsafe-promise-catch-error-property.test.ts | 5 +++++ .../no-unsafe-promise-catch-error-property.ts | 5 ----- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pr-triage-agent.lock.yml b/.github/workflows/pr-triage-agent.lock.yml index 8776ef0d21b..f03d2293e1e 100644 --- a/.github/workflows/pr-triage-agent.lock.yml +++ b/.github/workflows/pr-triage-agent.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"13b159b3008ccce2d9c7233e7bfa0c5eea0583c57bd721b9ee9a33efa43bae5f","body_hash":"8d505062b276bc9c6fda2b79316117cb30c88ae04cab333ef78d113bf91c6aec","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.65","copilot-sdk":"1.0.4"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.16"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.16"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.16"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.16"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.32","digest":"sha256:63e46b56dfd70895a701b6fc6dd0189e11e2d875f327f1781e81b31848735477","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.32@sha256:63e46b56dfd70895a701b6fc6dd0189e11e2d875f327f1781e81b31848735477"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.15"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.15"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.15"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.15"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.32","digest":"sha256:63e46b56dfd70895a701b6fc6dd0189e11e2d875f327f1781e81b31848735477","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.32@sha256:63e46b56dfd70895a701b6fc6dd0189e11e2d875f327f1781e81b31848735477"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ diff --git a/eslint-factory/src/rules/no-unsafe-catch-error-property.test.ts b/eslint-factory/src/rules/no-unsafe-catch-error-property.test.ts index 72c9eae1ba4..9c416c8f65d 100644 --- a/eslint-factory/src/rules/no-unsafe-catch-error-property.test.ts +++ b/eslint-factory/src/rules/no-unsafe-catch-error-property.test.ts @@ -391,6 +391,23 @@ try { }, ], }, + { + // Standalone err !== null in a separate if (without return) does not count as companion guard + code: `try { f(); } catch (err) { if (err !== null) { } if (typeof err === 'object') { console.log(err.status); } }`, + errors: [ + { + messageId: "unsafeProperty", + data: { prop: "status", errorVar: "err" }, + suggestions: [ + { + messageId: "wrapWithInstanceof", + data: { errorVar: "err", prop: "status" }, + output: `try { f(); } catch (err) { if (err !== null) { } if (typeof err === 'object') { console.log((err instanceof Error ? err.status : undefined)); } }`, + }, + ], + }, + ], + }, ], }); }); diff --git a/eslint-factory/src/rules/no-unsafe-catch-error-property.ts b/eslint-factory/src/rules/no-unsafe-catch-error-property.ts index 0c880f29313..94865b531b0 100644 --- a/eslint-factory/src/rules/no-unsafe-catch-error-property.ts +++ b/eslint-factory/src/rules/no-unsafe-catch-error-property.ts @@ -126,11 +126,6 @@ export const noUnsafeCatchErrorPropertyRule = createRule({ return; } - if (isNonNullGuardCheck(node, top.varName)) { - top.hasNonNullGuard = true; - return; - } - if (isTypeofObjectCheck(node, top.varName) && top.hasNonNullGuard) { top.hasGuard = true; } diff --git a/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.test.ts b/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.test.ts index b5e8746ef4f..13bb44f0a10 100644 --- a/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.test.ts +++ b/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.test.ts @@ -265,6 +265,11 @@ describe("no-unsafe-promise-catch-error-property", () => { code: `promise.catch(err => { if ('object' === typeof err) { console.log(err.status); } });`, errors: [{ messageId: "unsafeProperty", data: { prop: "status", errorVar: "err" } }], }, + { + // Standalone err !== null in a separate if (without return) does not count as companion guard + code: `promise.catch(err => { if (err !== null) { } if (typeof err === 'object') { console.log(err.status); } });`, + errors: [{ messageId: "unsafeProperty", data: { prop: "status", errorVar: "err" } }], + }, ], }); }); diff --git a/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.ts b/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.ts index 0ba4f8c4da4..8a733333fe7 100644 --- a/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.ts +++ b/eslint-factory/src/rules/no-unsafe-promise-catch-error-property.ts @@ -137,11 +137,6 @@ export const noUnsafePromiseCatchErrorPropertyRule = createRule({ return; } - if (isNonNullGuardCheck(node, top.varName)) { - top.hasNonNullGuard = true; - return; - } - if (isTypeofObjectCheck(node, top.varName) && top.hasNonNullGuard) { top.hasGuard = true; }