-
-
Notifications
You must be signed in to change notification settings - Fork 367
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
2,697 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} // ✅ | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}; |
Oops, something went wrong.