diff --git a/packages/eslint-plugin-prefer-let/README.md b/packages/eslint-plugin-prefer-let/README.md index efcde86..5630bdd 100644 --- a/packages/eslint-plugin-prefer-let/README.md +++ b/packages/eslint-plugin-prefer-let/README.md @@ -82,6 +82,38 @@ Then configure the rules you want to use under the rules section. } ``` +### Options + +#### `forceUpperCaseConst` + +When set to `true`, this option enforces `const` for top-level `UPPER_CASE` names (e.g. `PI`, `API_BASE_URL`) + +```json +{ + "rules": { + "prefer-let/prefer-let": [2, { "forceUpperCaseConst": true }] + } +} +``` + +This makes the distinction between true constants and regular bindings explicit and machine-enforced. + +Good: + +```javascript +const PI = 3.14; +const API_BASE_URL = 'https://example.com'; + +let config = loadConfig(); +``` + +Bad: + +```javascript +const config = loadConfig(); // not UPPER_CASE — use let +let PI = 3.14; // UPPER_CASE — use const +``` + ### Possible Conflicts This plugin may conflict with other plugins or configs that set `eslint prefer-const`. You can configure the rules to avoid this: diff --git a/packages/eslint-plugin-prefer-let/lib/rules/prefer-let.js b/packages/eslint-plugin-prefer-let/lib/rules/prefer-let.js index 200937a..41349e8 100644 --- a/packages/eslint-plugin-prefer-let/lib/rules/prefer-let.js +++ b/packages/eslint-plugin-prefer-let/lib/rules/prefer-let.js @@ -17,12 +17,22 @@ module.exports = { }, fixable: "code", // or "code" or "whitespace" schema: [ - // fill in your schema + { + type: "object", + properties: { + forceUpperCaseConst: { + type: "boolean" + } + }, + additionalProperties: false + } ] }, create: function(context) { let sourceCode = context.sourceCode ?? context.getSourceCode(); + let options = context.options[0] || {}; + let forceUpperCaseConst = options.forceUpperCaseConst || false; //---------------------------------------------------------------------- // Helpers @@ -51,6 +61,40 @@ module.exports = { return isGlobalScope(node) || isModuleScope(node) || isProgramScope(node); } + function isUpperCase(name) { + return /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/.test(name); + } + + function getBindingNames(node) { + if (node.type === 'Identifier') { + return [node.name]; + } + if (node.type === 'ObjectPattern') { + return node.properties.flatMap(function(prop) { + return getBindingNames(prop.value || prop.argument); + }); + } + if (node.type === 'ArrayPattern') { + return node.elements.filter(Boolean).flatMap(function(el) { + return getBindingNames(el); + }); + } + if (node.type === 'RestElement') { + return getBindingNames(node.argument); + } + if (node.type === 'AssignmentPattern') { + return getBindingNames(node.left); + } + return []; + } + + function allDeclaratorsUpperCase(node) { + return node.declarations.every(function(decl) { + let names = getBindingNames(decl.id); + return names.length > 0 && names.every(isUpperCase); + }); + } + function isInAmbientContext(node) { let current = node.parent; while (current) { @@ -76,14 +120,35 @@ module.exports = { message: 'prefer `let` over `var` to declare value bindings', node }); - } else if (node.kind === 'const' && !isTopLevelScope(node)) { - let constToken = sourceCode.getFirstToken(node); - + } else if (node.kind === 'const') { + if (isTopLevelScope(node)) { + if (forceUpperCaseConst && !allDeclaratorsUpperCase(node)) { + let constToken = sourceCode.getFirstToken(node); + context.report({ + message: '`const` declaration for non-constant names at top-level scope. Use `let` or rename to UPPER_CASE', + node, + fix: function(fixer) { + return fixer.replaceText(constToken, 'let'); + } + }); + } + } else { + let constToken = sourceCode.getFirstToken(node); + context.report({ + message: '`const` declaration outside top-level scope', + node, + fix: function(fixer) { + return fixer.replaceText(constToken, 'let'); + } + }); + } + } else if (node.kind === 'let' && forceUpperCaseConst && isTopLevelScope(node) && allDeclaratorsUpperCase(node)) { + let letToken = sourceCode.getFirstToken(node); context.report({ - message: '`const` declaration outside top-level scope', + message: 'use `const` for constant names (UPPER_CASE) at top-level scope', node, fix: function(fixer) { - return fixer.replaceText(constToken, 'let'); + return fixer.replaceText(letToken, 'const'); } }); } diff --git a/packages/eslint-plugin-prefer-let/tests/lib/rules/prefer-let.js b/packages/eslint-plugin-prefer-let/tests/lib/rules/prefer-let.js index 8e0ecef..a54d0f4 100644 --- a/packages/eslint-plugin-prefer-let/tests/lib/rules/prefer-let.js +++ b/packages/eslint-plugin-prefer-let/tests/lib/rules/prefer-let.js @@ -64,7 +64,31 @@ ruleTester.run("prefer-let", rule, { languageOptions: { parser: require("@typescript-eslint/parser") } - } + }, + // forceUpperCaseConst: const with UPPER_CASE at top level is valid + { + code: "const FOO_BAR = 'baz';", + options: [{ forceUpperCaseConst: true }] + }, + { + code: "const FOO = 1;", + options: [{ forceUpperCaseConst: true }] + }, + { + code: "const API_BASE_URL = 'http://example.com';", + options: [{ forceUpperCaseConst: true }] + }, + { + code: "export const FOO_BAR = 'baz';", + options: [{ forceUpperCaseConst: true }] + }, + // forceUpperCaseConst: let with non-upper-case at top level is valid + { + code: "let fooBar = 'baz';", + options: [{ forceUpperCaseConst: true }] + }, + // forceUpperCaseConst: const UPPER_CASE inside function is still invalid + // (no special treatment for non-top-level) ], invalid: [ @@ -113,5 +137,56 @@ ruleTester.run("prefer-let", rule, { message: "`const` declaration outside top-level scope" }] }, + // forceUpperCaseConst: const with non-upper-case at top level is invalid + { + code: "const fooBar = 'baz';", + output: "let fooBar = 'baz';", + options: [{ forceUpperCaseConst: true }], + errors: [{ + message: "`const` declaration for non-constant names at top-level scope. Use `let` or rename to UPPER_CASE" + }] + }, + { + code: "const { foo, bar } = {};", + output: "let { foo, bar } = {};", + options: [{ forceUpperCaseConst: true }], + errors: [{ + message: "`const` declaration for non-constant names at top-level scope. Use `let` or rename to UPPER_CASE" + }] + }, + { + code: "export const AlsoObject = Object;", + output: "export let AlsoObject = Object;", + options: [{ forceUpperCaseConst: true }], + errors: [{ + message: "`const` declaration for non-constant names at top-level scope. Use `let` or rename to UPPER_CASE" + }] + }, + // forceUpperCaseConst: let with UPPER_CASE at top level is invalid + { + code: "let FOO_BAR = 'baz';", + output: "const FOO_BAR = 'baz';", + options: [{ forceUpperCaseConst: true }], + errors: [{ + message: "use `const` for constant names (UPPER_CASE) at top-level scope" + }] + }, + { + code: "let FOO = 1;", + output: "const FOO = 1;", + options: [{ forceUpperCaseConst: true }], + errors: [{ + message: "use `const` for constant names (UPPER_CASE) at top-level scope" + }] + }, + // forceUpperCaseConst: const UPPER_CASE inside function is still invalid + { + code: "function y() { const FOO_BAR = 'baz'; return FOO_BAR; }", + output: "function y() { let FOO_BAR = 'baz'; return FOO_BAR; }", + options: [{ forceUpperCaseConst: true }], + errors: [{ + message: "`const` declaration outside top-level scope" + }] + }, ] });