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
66 changes: 56 additions & 10 deletions eslint-factory/src/rules/no-err-stack-then-string-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe("no-err-stack-then-string-fallback", () => {
valid: [],
invalid: [
{
code: `const msg = err instanceof Error ? err.stack : String(err);`,
code: `const { getErrorMessage } = require("./error_helpers.cjs"); const msg = err instanceof Error ? err.stack : String(err);`,
errors: [
{
messageId: "preferGetErrorMessage",
Expand All @@ -73,7 +73,7 @@ describe("no-err-stack-then-string-fallback", () => {
{
messageId: "replaceWithGetErrorMessage",
data: { errorVar: "err" },
output: `const msg = getErrorMessage(err);`,
output: `const { getErrorMessage } = require("./error_helpers.cjs"); const msg = getErrorMessage(err);`,
},
],
},
Expand All @@ -88,7 +88,7 @@ describe("no-err-stack-then-string-fallback", () => {
valid: [],
invalid: [
{
code: "core.setFailed(`unhandled error: ${err instanceof Error ? err.stack : String(err)}`);",
code: `const { getErrorMessage } = require("./error_helpers.cjs"); core.setFailed(\`unhandled error: \${err instanceof Error ? err.stack : String(err)}\`);`,
errors: [
{
messageId: "preferGetErrorMessage",
Expand All @@ -97,7 +97,7 @@ describe("no-err-stack-then-string-fallback", () => {
{
messageId: "replaceWithGetErrorMessage",
data: { errorVar: "err" },
output: "core.setFailed(`unhandled error: ${getErrorMessage(err)}`);",
output: `const { getErrorMessage } = require("./error_helpers.cjs"); core.setFailed(\`unhandled error: \${getErrorMessage(err)}\`);`,
},
],
},
Expand All @@ -119,7 +119,7 @@ describe("no-err-stack-then-string-fallback", () => {
valid: [],
invalid: [
{
code: `core.setFailed(err && err.stack ? err.stack : String(err));`,
code: `const { getErrorMessage } = require("./error_helpers.cjs"); core.setFailed(err && err.stack ? err.stack : String(err));`,
errors: [
{
messageId: "preferGetErrorMessage",
Expand All @@ -128,7 +128,7 @@ describe("no-err-stack-then-string-fallback", () => {
{
messageId: "replaceWithGetErrorMessage",
data: { errorVar: "err" },
output: `core.setFailed(getErrorMessage(err));`,
output: `const { getErrorMessage } = require("./error_helpers.cjs"); core.setFailed(getErrorMessage(err));`,
},
],
},
Expand All @@ -143,7 +143,7 @@ describe("no-err-stack-then-string-fallback", () => {
valid: [],
invalid: [
{
code: `const msg = err && err.stack ? err.stack : String(err);`,
code: `const { getErrorMessage } = require("./error_helpers.cjs"); const msg = err && err.stack ? err.stack : String(err);`,
errors: [
{
messageId: "preferGetErrorMessage",
Expand All @@ -152,7 +152,7 @@ describe("no-err-stack-then-string-fallback", () => {
{
messageId: "replaceWithGetErrorMessage",
data: { errorVar: "err" },
output: `const msg = getErrorMessage(err);`,
output: `const { getErrorMessage } = require("./error_helpers.cjs"); const msg = getErrorMessage(err);`,
},
],
},
Expand All @@ -167,7 +167,7 @@ describe("no-err-stack-then-string-fallback", () => {
valid: [],
invalid: [
{
code: `console.error(error && error.stack ? error.stack : String(error));`,
code: `const { getErrorMessage } = require("./error_helpers.cjs"); console.error(error && error.stack ? error.stack : String(error));`,
errors: [
{
messageId: "preferGetErrorMessage",
Expand All @@ -176,7 +176,7 @@ describe("no-err-stack-then-string-fallback", () => {
{
messageId: "replaceWithGetErrorMessage",
data: { errorVar: "error" },
output: `console.error(getErrorMessage(error));`,
output: `const { getErrorMessage } = require("./error_helpers.cjs"); console.error(getErrorMessage(error));`,
},
],
},
Expand All @@ -185,4 +185,50 @@ describe("no-err-stack-then-string-fallback", () => {
],
});
});

it("invalid: getErrorMessage defined in its own initializer (TDZ) — diagnostic fires but no suggestion offered", () => {
cjsRuleTester.run("no-err-stack-then-string-fallback", noErrStackThenStringFallbackRule, {
valid: [],
invalid: [
{
code: `const getErrorMessage = err instanceof Error ? err.stack : String(err);`,
errors: [
{
messageId: "preferGetErrorMessage",
data: { errorVar: "err" },
suggestions: [],
},
],
},
],
});
});

it("invalid: no getErrorMessage in scope — diagnostic fires but no suggestion offered", () => {
cjsRuleTester.run("no-err-stack-then-string-fallback", noErrStackThenStringFallbackRule, {
valid: [],
invalid: [
{
code: `const msg = err instanceof Error ? err.stack : String(err);`,
errors: [
{
messageId: "preferGetErrorMessage",
data: { errorVar: "err" },
suggestions: [],
},
],
},
{
code: `process.stderr.write(\`[copilot-sdk-driver] unhandled error: \${err instanceof Error ? err.stack : String(err)}\\n\`);`,
errors: [
{
messageId: "preferGetErrorMessage",
data: { errorVar: "err" },
suggestions: [],
},
],
},
],
});
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Tests only cover the CJS require path — the ESM ImportBinding branch of isDefinitionAvailableAtNode is untested.

isDefinitionAvailableAtNode returns true immediately for ImportBinding, so the suggestion will fire for ESM imports too. Adding one test case with import { getErrorMessage } from '...' would confirm that branch and prevent a silent regression if the early-return is removed.

💡 Suggested test snippet
{
  code: `import { getErrorMessage } from './error_helpers.cjs'; const msg = err instanceof Error ? err.stack : String(err);`,
  errors: [{
    messageId: 'preferGetErrorMessage',
    data: { errorVar: 'err' },
    suggestions: [{ messageId: 'replaceWithGetErrorMessage', data: { errorVar: 'err' }, output: `import { getErrorMessage } from './error_helpers.cjs'; const msg = getErrorMessage(err);` }],
  }],
}

Use an ESM rule tester instance with { parserOptions: { sourceType: 'module' } }.

@copilot please address this.

53 changes: 43 additions & 10 deletions eslint-factory/src/rules/no-err-stack-then-string-fallback.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AST_NODE_TYPES, ESLintUtils, TSESTree } from "@typescript-eslint/utils";
import { AST_NODE_TYPES, ESLintUtils, TSESLint, TSESTree } from "@typescript-eslint/utils";

const createRule = ESLintUtils.RuleCreator(name => `https://github.com/github/gh-aw/tree/main/eslint-factory#${name}`);

Expand Down Expand Up @@ -43,6 +43,22 @@ function isStringErr(node: TSESTree.Node, errVar: string): boolean {
return isIdentifierNamed(node.arguments[0], errVar);
}

function isDefinitionAvailableAtNode(definition: TSESLint.Scope.Definition, node: TSESTree.Node): boolean {
if (definition.type === "ImportBinding" || definition.type === "FunctionName") {
return true;
}
const definitionNode = definition.name ?? definition.node;
if (!definitionNode?.range || !node.range) return false;
if (definitionNode.range[0] >= node.range[0]) return false;
// If the node falls inside the variable declarator's range, the binding is in the
// temporal dead zone at that point (e.g. `const getErrorMessage = <node>`).
const declNode = definition.node;
if (declNode?.range && node.range[0] >= declNode.range[0] && node.range[1] <= declNode.range[1]) {
return false;
}
return true;
}

export const noErrStackThenStringFallbackRule = createRule({
name: "no-err-stack-then-string-fallback",
meta: {
Expand All @@ -63,6 +79,21 @@ export const noErrStackThenStringFallbackRule = createRule({
},
defaultOptions: [],
create(context) {
const sourceCode = context.sourceCode;
type SourceCodeScope = ReturnType<typeof sourceCode.getScope>;

function hasResolvableLocalBinding(node: TSESTree.Node, name: string): boolean {
let scope: SourceCodeScope | null = sourceCode.getScope(node);
while (scope) {
const variable = scope.set.get(name);
if (variable && variable.defs.some(def => isDefinitionAvailableAtNode(def, node))) {
return true;
}
scope = scope.upper;
}
return false;
}

return {
ConditionalExpression(node) {
// Patterns:
Expand Down Expand Up @@ -91,15 +122,17 @@ export const noErrStackThenStringFallbackRule = createRule({
node,
messageId: "preferGetErrorMessage",
data: { errorVar: errVar },
suggest: [
{
messageId: "replaceWithGetErrorMessage",
data: { errorVar: errVar },
fix(fixer) {
return fixer.replaceText(node, `getErrorMessage(${errVar})`);
},
},
],
suggest: hasResolvableLocalBinding(node, "getErrorMessage")
? [
{
messageId: "replaceWithGetErrorMessage",
data: { errorVar: errVar },
fix(fixer) {
return fixer.replaceText(node, `getErrorMessage(${errVar})`);
},
},
]
: [],
});
},
};
Expand Down
Loading