From 265c2569ef0c2758dccaefd57c91f2f6a333dd3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:23:00 +0000 Subject: [PATCH 1/2] Add duplicate constant value ESLint rule Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- eslint-factory/README.md | 7 ++ eslint-factory/eslint.config.cjs | 1 + eslint-factory/src/index.ts | 2 + .../no-duplicate-constant-values.test.ts | 69 ++++++++++++++++ .../src/rules/no-duplicate-constant-values.ts | 81 +++++++++++++++++++ 5 files changed, 160 insertions(+) create mode 100644 eslint-factory/src/rules/no-duplicate-constant-values.test.ts create mode 100644 eslint-factory/src/rules/no-duplicate-constant-values.ts diff --git a/eslint-factory/README.md b/eslint-factory/README.md index 62a74d6a65f..1fac0ddbdde 100644 --- a/eslint-factory/README.md +++ b/eslint-factory/README.md @@ -20,6 +20,7 @@ This project hosts custom ESLint linters for `/actions/setup/js`. |---|---| | [`no-core-exportvariable-non-string`](#no-core-exportvariable-non-string) | Require explicit string values for `core.exportVariable` calls | | [`no-core-setoutput-non-string`](#no-core-setoutput-non-string) | Require explicit string values for `core.setOutput` calls | +| [`no-duplicate-constant-values`](#no-duplicate-constant-values) | Report constants with duplicate static primitive values in the same file | | [`no-child-process-interpolated-command`](#no-child-process-interpolated-command) | Disallow interpolated command strings in shell-evaluated `child_process` calls | | [`no-github-request-interpolated-route`](#no-github-request-interpolated-route) | Disallow interpolated route arguments in Octokit `.request()` calls | | [`no-json-stringify-error`](#no-json-stringify-error) | Disallow `JSON.stringify()` on caught error variables | @@ -44,6 +45,12 @@ This project hosts custom ESLint linters for `/actions/setup/js`. | [`require-execfilesync-try-catch`](#require-execfilesync-try-catch) | Require try/catch around `execFileSync(...)` calls from `child_process` | | [`require-spawnsync-error-check`](#require-spawnsync-error-check) | Require checking `result.error` after `spawnSync` calls | +### `no-duplicate-constant-values` + +Inventory module-level `const` declarations with static primitive initializers and report each declaration after the first one that uses the same value in a file. The diagnostic names both constants and shows the duplicated value. + +The rule compares string, number, boolean, `null`, bigint, regular-expression, static template-literal, and signed numeric initializers. Dynamic expressions, object and array literals, destructuring declarations, function-local declarations, and `let` or `var` declarations are ignored. + ### `no-github-request-interpolated-route` Disallow template literals with interpolations or string concatenation expressions as the route argument of Octokit `.request()` calls. diff --git a/eslint-factory/eslint.config.cjs b/eslint-factory/eslint.config.cjs index dc397768ce5..65462d4da8f 100644 --- a/eslint-factory/eslint.config.cjs +++ b/eslint-factory/eslint.config.cjs @@ -45,6 +45,7 @@ module.exports = [ "gh-aw-custom/no-caught-error-interpolation": "warn", "gh-aw-custom/require-fetch-try-catch": "warn", "gh-aw-custom/no-core-error-then-setfailed": "warn", + "gh-aw-custom/no-duplicate-constant-values": "warn", }, }, { diff --git a/eslint-factory/src/index.ts b/eslint-factory/src/index.ts index 7bfcf7daaca..95ffdafa44b 100644 --- a/eslint-factory/src/index.ts +++ b/eslint-factory/src/index.ts @@ -31,6 +31,7 @@ import { noErrStackThenStringFallbackRule } from "./rules/no-err-stack-then-stri import { noCaughtErrorInterpolationRule } from "./rules/no-caught-error-interpolation"; import { requireFetchTryCatchRule } from "./rules/require-fetch-try-catch"; import { noCoreErrorThenSetFailedRule } from "./rules/no-core-error-then-setfailed"; +import { noDuplicateConstantValuesRule } from "./rules/no-duplicate-constant-values"; const plugin = { meta: { @@ -71,6 +72,7 @@ const plugin = { "no-caught-error-interpolation": noCaughtErrorInterpolationRule, "require-fetch-try-catch": requireFetchTryCatchRule, "no-core-error-then-setfailed": noCoreErrorThenSetFailedRule, + "no-duplicate-constant-values": noDuplicateConstantValuesRule, }, }; diff --git a/eslint-factory/src/rules/no-duplicate-constant-values.test.ts b/eslint-factory/src/rules/no-duplicate-constant-values.test.ts new file mode 100644 index 00000000000..6a289bc030e --- /dev/null +++ b/eslint-factory/src/rules/no-duplicate-constant-values.test.ts @@ -0,0 +1,69 @@ +import { RuleTester } from "eslint"; +import { describe, expect, it } from "vitest"; +import { noDuplicateConstantValuesRule } from "./no-duplicate-constant-values"; + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: "commonjs", + }, +}); + +describe("no-duplicate-constant-values", () => { + it("uses the correct docs URL", () => { + expect(noDuplicateConstantValuesRule.meta.docs.url).toBe("https://github.com/github/gh-aw/tree/main/eslint-factory#no-duplicate-constant-values"); + }); + + it("accepts unique and dynamic constant values", () => { + ruleTester.run("no-duplicate-constant-values", noDuplicateConstantValuesRule, { + valid: [ + `const FIRST = "first"; const SECOND = "second";`, + `const FIRST = makeValue(); const SECOND = makeValue();`, + `const FIRST = { value: 1 }; const SECOND = { value: 1 };`, + `let first = "same"; let second = "same";`, + `const { first, second } = value;`, + `function first() { const VALUE = "same"; } function second() { const VALUE = "same"; }`, + ], + invalid: [], + }); + }); + + it("reports duplicate strings, numbers, templates, and regular expressions", () => { + ruleTester.run("no-duplicate-constant-values", noDuplicateConstantValuesRule, { + valid: [], + invalid: [ + { + code: `const FIRST = "same"; const SECOND = "same";`, + errors: [{ messageId: "duplicateConstantValue", data: { name: "SECOND", originalName: "FIRST", value: `"same"` } }], + }, + { + code: 'const FIRST = `same`; const SECOND = "same";', + errors: [{ messageId: "duplicateConstantValue", data: { name: "SECOND", originalName: "FIRST", value: `"same"` } }], + }, + { + code: `const FIRST = -42; const SECOND = -42; const THIRD = 42;`, + errors: [{ messageId: "duplicateConstantValue", data: { name: "SECOND", originalName: "FIRST", value: "-42" } }], + }, + { + code: `const FIRST = /value/gi; const SECOND = /value/gi;`, + errors: [{ messageId: "duplicateConstantValue", data: { name: "SECOND", originalName: "FIRST", value: "/value/gi" } }], + }, + ], + }); + }); + + it("reports every duplicate after the first declaration", () => { + ruleTester.run("no-duplicate-constant-values", noDuplicateConstantValuesRule, { + valid: [], + invalid: [ + { + code: `const FIRST = "same"; const SECOND = "same"; const THIRD = "same";`, + errors: [ + { messageId: "duplicateConstantValue", data: { name: "SECOND", originalName: "FIRST", value: `"same"` } }, + { messageId: "duplicateConstantValue", data: { name: "THIRD", originalName: "FIRST", value: `"same"` } }, + ], + }, + ], + }); + }); +}); diff --git a/eslint-factory/src/rules/no-duplicate-constant-values.ts b/eslint-factory/src/rules/no-duplicate-constant-values.ts new file mode 100644 index 00000000000..cb255a5643b --- /dev/null +++ b/eslint-factory/src/rules/no-duplicate-constant-values.ts @@ -0,0 +1,81 @@ +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from "@typescript-eslint/utils"; + +const createRule = ESLintUtils.RuleCreator(name => `https://github.com/github/gh-aw/tree/main/eslint-factory#${name}`); + +interface ConstantDeclaration { + name: string; +} + +function getStaticValueKey(node: TSESTree.Expression): string | null { + if (node.type === AST_NODE_TYPES.Literal) { + if ("regex" in node && node.regex) { + return `regexp:${node.regex.pattern}/${node.regex.flags}`; + } + return `${typeof node.value}:${String(node.value)}`; + } + + if (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0) { + return `string:${node.quasis[0].value.cooked ?? node.quasis[0].value.raw}`; + } + + if (node.type === AST_NODE_TYPES.UnaryExpression && (node.operator === "+" || node.operator === "-") && node.argument.type === AST_NODE_TYPES.Literal && typeof node.argument.value === "number") { + return `number:${String(node.operator === "-" ? -node.argument.value : node.argument.value)}`; + } + + return null; +} + +export const noDuplicateConstantValuesRule = createRule({ + name: "no-duplicate-constant-values", + meta: { + type: "suggestion", + docs: { + description: "List module-level constant declarations by their static primitive values and report later declarations that duplicate a value in the same file.", + }, + schema: [], + messages: { + duplicateConstantValue: "Constant '{{name}}' duplicates the value of constant '{{originalName}}' ({{value}}).", + }, + }, + defaultOptions: [], + create(context) { + const constantsByValue = new Map(); + + return { + VariableDeclaration(node) { + if (node.kind !== "const" || node.parent.type !== AST_NODE_TYPES.Program) { + return; + } + + for (const declaration of node.declarations) { + if (declaration.id.type !== AST_NODE_TYPES.Identifier || !declaration.init) { + continue; + } + + const valueKey = getStaticValueKey(declaration.init); + if (valueKey === null) { + continue; + } + + const original = constantsByValue.get(valueKey); + if (!original) { + constantsByValue.set(valueKey, { + name: declaration.id.name, + }); + continue; + } + + context.report({ + node: declaration, + messageId: "duplicateConstantValue", + data: { + name: declaration.id.name, + originalName: original.name, + value: context.sourceCode.getText(declaration.init), + }, + }); + } + }, + }; + }, +}); From 92d3df0a511c72894b4ec3299e6f325a580d01f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:14:24 +0000 Subject: [PATCH 2/2] fix: canonicalize regex flags in no-duplicate-constant-values rule Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- eslint-factory/src/rules/no-duplicate-constant-values.test.ts | 4 ++++ eslint-factory/src/rules/no-duplicate-constant-values.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/eslint-factory/src/rules/no-duplicate-constant-values.test.ts b/eslint-factory/src/rules/no-duplicate-constant-values.test.ts index 6a289bc030e..ff19cfb6cb2 100644 --- a/eslint-factory/src/rules/no-duplicate-constant-values.test.ts +++ b/eslint-factory/src/rules/no-duplicate-constant-values.test.ts @@ -48,6 +48,10 @@ describe("no-duplicate-constant-values", () => { code: `const FIRST = /value/gi; const SECOND = /value/gi;`, errors: [{ messageId: "duplicateConstantValue", data: { name: "SECOND", originalName: "FIRST", value: "/value/gi" } }], }, + { + code: `const FIRST = /value/gi; const SECOND = /value/ig;`, + errors: [{ messageId: "duplicateConstantValue", data: { name: "SECOND", originalName: "FIRST", value: "/value/ig" } }], + }, ], }); }); diff --git a/eslint-factory/src/rules/no-duplicate-constant-values.ts b/eslint-factory/src/rules/no-duplicate-constant-values.ts index cb255a5643b..0eeef5c5722 100644 --- a/eslint-factory/src/rules/no-duplicate-constant-values.ts +++ b/eslint-factory/src/rules/no-duplicate-constant-values.ts @@ -9,7 +9,7 @@ interface ConstantDeclaration { function getStaticValueKey(node: TSESTree.Expression): string | null { if (node.type === AST_NODE_TYPES.Literal) { if ("regex" in node && node.regex) { - return `regexp:${node.regex.pattern}/${node.regex.flags}`; + return `regexp:${node.regex.pattern}/${[...node.regex.flags].sort().join("")}`; } return `${typeof node.value}:${String(node.value)}`; }