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
32 changes: 32 additions & 0 deletions packages/eslint-plugin-prefer-let/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
77 changes: 71 additions & 6 deletions packages/eslint-plugin-prefer-let/lib/rules/prefer-let.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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');
}
});
}
Expand Down
77 changes: 76 additions & 1 deletion packages/eslint-plugin-prefer-let/tests/lib/rules/prefer-let.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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"
}]
},
]
});
Loading