Skip to content

Commit

Permalink
Add better-boolean-var-names
Browse files Browse the repository at this point in the history
  • Loading branch information
axetroy committed Sep 8, 2024
1 parent 891842d commit bc3134e
Show file tree
Hide file tree
Showing 11 changed files with 2,697 additions and 0 deletions.
145 changes: 145 additions & 0 deletions docs/rules/better-boolean-variable-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Prefer readable Boolean variable names

💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs-eslintconfigjs).

💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).

<!-- end auto-generated rule header -->
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->

When it is clear that an expression is a Boolean value, the variable name should start with `is`/`was`/`has`/`can`/`should` to improve readability.

## Examples

```js
const completed = true; //
const isCompleted = true; //
```

```js
const completed = progress === 100; //
const isCompleted = progress === 100; //
```

```js
const completed = Boolean('true'); //
const isCompleted = Boolean('true'); //
```

```js
const completed = new Boolean('true'); //
const isCompleted = new Boolean('true'); //
```

```js
const adult = age >= 18; //
const isAdult = age >= 18; //
```

```js
const adult = age >= 18 ? true : false; //
const isAdult = age >= 18 ? true : false; //
```

```js
const gotModifyRights = isGotPreviewRights() && isGotDownloadRights(); //
const isGotModifyRights = isGotPreviewRights() && isGotDownloadRights(); //
```

```js
const showingModal = !!modalElement; //
const isShowingModal = !!modalElement; //
```

```js
const showingModal = (this.showingModal = true); //
const isShowingModal = (this.showingModal = true); //
```

```js
const showingModal = (doSomething(), !!modalElement); //
const isShowingModal = (doSomething(), !!modalElement); //
```

```js
//
async function foo() {
const completed = await progress === 100;
}

//
async function foo() {
const isCompleted = await progress === 100;
}
```

```js
//
function* foo() {
const completed = yield progress === 100;
}

//
function* foo() {
const isCompleted = yield progress === 100;
}
```

```js
//
const isCompleted = true
const downloaded = isCompleted

//
const isCompleted = true
const isDownloaded = isCompleted
```

<!-- Type Annotation -->
## Type Annotation

```js
const completed = isCompleted as boolean; //
const isCompleted = isCompleted as boolean; //
```

```js
const completed = isCompleted() as boolean; //
const isCompleted = isCompleted() as boolean; //
```

```js
//
var isCompleted: boolean
const downloaded = isCompleted

//
var isCompleted: boolean
const isDownloaded = isCompleted
```

```js
//
function isCompleted(): boolean {}
const downloaded = isCompleted()

//
function isCompleted(): boolean {}
const isDownloaded = isCompleted()
```

```js
function completed(): boolean {} //
function isCompleted(): boolean {} //
```

```js
const completed = (): boolean => {} //
const isCompleted = (): boolean => {} //
```

```js
function download(url: string, showProgress: boolean) {} //

function download(url: string, shouldShowProgress: boolean) {} //
```
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c

| Name                                    | Description | 💼 | 🔧 | 💡 |
| :----------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :- | :- | :- |
| [better-boolean-variable-names](docs/rules/better-boolean-variable-names.md) | Prefer readable Boolean variable names || | 💡 |
| [better-regex](docs/rules/better-regex.md) | Improve regexes by making them shorter, consistent, and safer. || 🔧 | |
| [catch-error-name](docs/rules/catch-error-name.md) | Enforce a specific parameter name in catch clauses. || 🔧 | |
| [consistent-destructuring](docs/rules/consistent-destructuring.md) | Use destructured variables over properties. | | 🔧 | 💡 |
Expand Down
171 changes: 171 additions & 0 deletions rules/better-boolean-variable-names.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
'use strict';

const renameVariable = require('./utils/rename-variable.js');
const {isBooleanExpression, isBooleanTypeAnnotation, isBooleanReturnTypeFunction} = require('./utils/is-boolean.js');

const MESSAGE_ID_ERROR = 'better-boolean-variable-names/error';
const MESSAGE_ID_SUGGESTION = 'better-boolean-variable-names/suggestion';
const messages = {
[MESSAGE_ID_ERROR]: 'Prefer readable Boolean variable names.',
[MESSAGE_ID_SUGGESTION]: 'Replace `{{value}}` with `{{replacement}}`.',
};

/**
Capitalize the first letter of a string
@param {string} str
@returns {string}
*/
function capitalize(string_) {
return string_.charAt(0).toUpperCase() + string_.slice(1);
}

/**
Extract underscores from the variable name
@param {string} variableName
@returns {[string, string]}
*/
function extractUnderscores(variableName) {
const underscoresRegex = /^_+/;

const match = variableName.match(underscoresRegex);
const underscores = match ? match[0] : '';
const nameWithoutUnderscores = variableName.replace(underscoresRegex, '');

return [underscores, nameWithoutUnderscores];
}

/**
@param {import('eslint').Rule.RuleContext} context
@returns {import('eslint').Rule.RuleListener}
*/
const create = context => {
const configuration = context.options[0] || {};
const BOOLEAN_PREFIXED = new Set(['is', 'was', 'has', 'can', 'should', ...(configuration.prefixes ?? [])]);

const replacement
= [...BOOLEAN_PREFIXED]
.slice(0, -1)
.map(v => `\`${v}\``)
.join(', ')
+ ', or '
+ [...BOOLEAN_PREFIXED].slice(-1).map(v => `\`${v}\``);

/**
Checks whether it is a readable Boolean identifier name
@param {string} identifier
@returns {boolean}
*/
function isValidBooleanVariableName(identifier) {
for (const prefix of BOOLEAN_PREFIXED) {
// Trim the leading underscores
identifier = identifier.replace(/^_+/, '');

if (identifier.startsWith(prefix) && identifier.length > prefix.length) {
return true;
}
}

return false;
}

/**
*
* @param {import('eslint').Rule.RuleContext} context
* @param {import('estree').Node} node
* @param {string} variableName
*/
function report(context, node, variableName) {
context.report({
node,
messageId: MESSAGE_ID_ERROR,
data: {
name: variableName,
prefixes: replacement,
},
suggest: [...BOOLEAN_PREFIXED].map(prefix => {
const [underscores, nameWithoutUnderscores] = extractUnderscores(variableName);

const isUpperCase = nameWithoutUnderscores.toUpperCase() === nameWithoutUnderscores;

const expectedVariableName = `${underscores}${isUpperCase ? prefix.toUpperCase() + '_' : prefix}${capitalize(nameWithoutUnderscores)}`;

return {
messageId: MESSAGE_ID_SUGGESTION,
data: {
value: variableName,
replacement: expectedVariableName,
},
* fix(fixer) {
yield * renameVariable(context.sourceCode, context.sourceCode.getScope(node), fixer, node, expectedVariableName);
},
};
}),
});
}

return {
VariableDeclarator(node) {
if (node.id.type === 'Identifier') {
const variableName = node.id.name;

// Const foo = (): boolean => {}
// const foo = function () {}
if (
['FunctionExpression', 'ArrowFunctionExpression'].includes(node.init?.type)
&& isBooleanReturnTypeFunction(node.init)
&& !isValidBooleanVariableName(variableName)
) {
report(context, node.id, variableName);
return;
}

if (
(isBooleanExpression(context, node.init) || isBooleanTypeAnnotation(node.id.typeAnnotation))
&& !isValidBooleanVariableName(variableName)
) {
report(context, node.id, variableName);
}
}
},
/**
@param {import('estree').Function} node
*/
'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression'(node) {
// Validate function name
if (node.id?.type === 'Identifier') {
const variableName = node.id.name;

if (isBooleanReturnTypeFunction(node) && !isValidBooleanVariableName(variableName)) {
report(context, node.id, variableName);
}
}

// Validate params
for (const parameter of node.params) {
if (parameter.type === 'Identifier') {
const variableName = parameter.name;
if (isBooleanTypeAnnotation(parameter.typeAnnotation) && !isValidBooleanVariableName(variableName)) {
report(context, parameter, variableName);
}
}
}
},
};
};

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer readable Boolean variable names',
recommended: true,
},
hasSuggestions: true,
messages,
},
};
Loading

0 comments on commit bc3134e

Please sign in to comment.