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 4c3f8bd6b28..d4469943e03 100644 --- a/eslint-factory/src/rules/no-duplicate-constant-values.test.ts +++ b/eslint-factory/src/rules/no-duplicate-constant-values.test.ts @@ -27,6 +27,8 @@ describe("no-duplicate-constant-values", () => { `const NOTIFY_TIMEOUT_MS = 10000; const KEEPALIVE_PING_INTERVAL_MS = 10000;`, `const A_PREFIX_LENGTH = 2; const B_PREFIX_LENGTH = 2;`, `const DEFAULT_HTTP_TIMEOUT_MS = 15000; const TOOL_CALL_TIMEOUT_BUFFER_MS = 15000; const NOTIFY_TIMEOUT_MS = 10000; const KEEPALIVE_PING_INTERVAL_MS = 10000;`, + `const FIRST_ENABLED = true; const SECOND_ENABLED = true;`, + `const FIRST_ENABLED = false; const SECOND_ENABLED = false;`, ], invalid: [], }); @@ -70,6 +72,21 @@ describe("no-duplicate-constant-values", () => { }); }); + it("requires at least three matching boolean constants before reporting duplicates", () => { + ruleTester.run("no-duplicate-constant-values", noDuplicateConstantValuesRule, { + valid: [`const FIRST_ENABLED = true; const SECOND_ENABLED = true;`], + invalid: [ + { + code: `const FIRST_ENABLED = true; const SECOND_ENABLED = true; const THIRD_ENABLED = true;`, + errors: [ + { messageId: "duplicateConstantValue", data: { name: "SECOND_ENABLED", originalName: "FIRST_ENABLED", value: "true" } }, + { messageId: "duplicateConstantValue", data: { name: "THIRD_ENABLED", originalName: "FIRST_ENABLED", value: "true" } }, + ], + }, + ], + }); + }); + it("reports every duplicate after the first declaration", () => { ruleTester.run("no-duplicate-constant-values", noDuplicateConstantValuesRule, { valid: [], diff --git a/eslint-factory/src/rules/no-duplicate-constant-values.ts b/eslint-factory/src/rules/no-duplicate-constant-values.ts index 3fa4907054c..80ed9ad97a7 100644 --- a/eslint-factory/src/rules/no-duplicate-constant-values.ts +++ b/eslint-factory/src/rules/no-duplicate-constant-values.ts @@ -8,6 +8,10 @@ interface ConstantDeclaration { } const MIN_NUMERIC_DUPLICATE_GROUP_SIZE = 3; +// Booleans only have 2 possible values module-wide, an even smaller space than "small numbers", +// so unrelated constants coincidentally sharing `true`/`false` are at least as likely as the +// numeric case above. Apply the same minimum-group-size guard to avoid false positives. +const MIN_BOOLEAN_DUPLICATE_GROUP_SIZE = 3; function getStaticValueKey(node: TSESTree.Expression): string | null { if (node.type === AST_NODE_TYPES.Literal) { @@ -79,7 +83,8 @@ export const noDuplicateConstantValuesRule = createRule({ }, "Program:exit"() { for (const [valueKey, declarations] of constantsByValue) { - const shouldReportDuplicates = declarations.length > 1 && (!valueKey.startsWith("number:") || declarations.length >= MIN_NUMERIC_DUPLICATE_GROUP_SIZE); + const minGroupSize = valueKey.startsWith("number:") ? MIN_NUMERIC_DUPLICATE_GROUP_SIZE : valueKey.startsWith("boolean:") ? MIN_BOOLEAN_DUPLICATE_GROUP_SIZE : 2; + const shouldReportDuplicates = declarations.length >= minGroupSize; if (!shouldReportDuplicates) continue; const original = declarations[0];