-
-
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.
Add
prefer-logical-operator-over-ternary
rule (#1830)
- Loading branch information
Showing
8 changed files
with
565 additions
and
3 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
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,52 @@ | ||
# Prefer using a logical operator over a ternary | ||
|
||
<!-- Do not manually modify RULE_NOTICE part. Run: `npm run generate-rule-notices` --> | ||
<!-- RULE_NOTICE --> | ||
✅ *This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.* | ||
|
||
💡 *This rule provides [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).* | ||
<!-- /RULE_NOTICE --> | ||
|
||
Disallow ternary operators when simpler logical operator alternatives exist. | ||
|
||
Ideally, most reported cases have an equivalent [`Logical OR(||)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR) expression. The rule intentionally provides suggestions instead of auto-fixes, because in many cases, the [nullish coalescing operator (`??`)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator) should be preferred. | ||
|
||
## Fail | ||
|
||
```js | ||
foo ? foo : bar; | ||
``` | ||
|
||
```js | ||
foo.bar ? foo.bar : foo.baz | ||
``` | ||
|
||
```js | ||
foo?.bar ? foo.bar : baz | ||
``` | ||
```js | ||
!bar ? foo : bar; | ||
``` | ||
## Pass | ||
```js | ||
foo ?? bar; | ||
``` | ||
```js | ||
foo || bar; | ||
``` | ||
```js | ||
foo ? bar : baz; | ||
``` | ||
```js | ||
foo.bar ?? foo.baz | ||
``` | ||
```js | ||
foo?.bar ?? baz | ||
``` |
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,155 @@ | ||
'use strict'; | ||
const {isParenthesized, getParenthesizedText} = require('./utils/parentheses.js'); | ||
const isSameReference = require('./utils/is-same-reference.js'); | ||
const shouldAddParenthesesToLogicalExpressionChild = require('./utils/should-add-parentheses-to-logical-expression-child.js'); | ||
const needsSemicolon = require('./utils/needs-semicolon.js'); | ||
|
||
const MESSAGE_ID_ERROR = 'prefer-logical-operator-over-ternary/error'; | ||
const MESSAGE_ID_SUGGESTION = 'prefer-logical-operator-over-ternary/suggestion'; | ||
const messages = { | ||
[MESSAGE_ID_ERROR]: 'Prefer using a logical operator over a ternary.', | ||
[MESSAGE_ID_SUGGESTION]: 'Switch to `{{operator}}` operator.', | ||
}; | ||
|
||
function isSameNode(left, right, sourceCode) { | ||
if (isSameReference(left, right)) { | ||
return true; | ||
} | ||
|
||
if (left.type !== right.type) { | ||
return false; | ||
} | ||
|
||
switch (left.type) { | ||
case 'AwaitExpression': | ||
return isSameNode(left.argument, right.argument, sourceCode); | ||
|
||
case 'LogicalExpression': | ||
return ( | ||
left.operator === right.operator | ||
&& isSameNode(left.left, right.left, sourceCode) | ||
&& isSameNode(left.right, right.right, sourceCode) | ||
); | ||
|
||
case 'UnaryExpression': | ||
return ( | ||
left.operator === right.operator | ||
&& left.prefix === right.prefix | ||
&& isSameNode(left.argument, right.argument, sourceCode) | ||
); | ||
|
||
case 'UpdateExpression': | ||
return false; | ||
|
||
// No default | ||
} | ||
|
||
return sourceCode.getText(left) === sourceCode.getText(right); | ||
} | ||
|
||
function fix({ | ||
fixer, | ||
sourceCode, | ||
conditionalExpression, | ||
left, | ||
right, | ||
operator, | ||
}) { | ||
let text = [left, right].map((node, index) => { | ||
const isNodeParenthesized = isParenthesized(node, sourceCode); | ||
let text = isNodeParenthesized ? getParenthesizedText(node, sourceCode) : sourceCode.getText(node); | ||
|
||
if ( | ||
!isNodeParenthesized | ||
&& shouldAddParenthesesToLogicalExpressionChild(node, {operator, property: index === 0 ? 'left' : 'right'}) | ||
) { | ||
text = `(${text})`; | ||
} | ||
|
||
return text; | ||
}).join(` ${operator} `); | ||
|
||
// According to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#table | ||
// There should be no cases need add parentheses when switching ternary to logical expression | ||
|
||
// ASI | ||
if (needsSemicolon(sourceCode.getTokenBefore(conditionalExpression), sourceCode, text)) { | ||
text = `;${text}`; | ||
} | ||
|
||
return fixer.replaceText(conditionalExpression, text); | ||
} | ||
|
||
function getProblem({ | ||
sourceCode, | ||
conditionalExpression, | ||
left, | ||
right, | ||
}) { | ||
return { | ||
node: conditionalExpression, | ||
messageId: MESSAGE_ID_ERROR, | ||
suggest: ['??', '||'].map(operator => ({ | ||
messageId: MESSAGE_ID_SUGGESTION, | ||
data: {operator}, | ||
fix: fixer => fix({ | ||
fixer, | ||
sourceCode, | ||
conditionalExpression, | ||
left, | ||
right, | ||
operator, | ||
}), | ||
})), | ||
}; | ||
} | ||
|
||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
|
||
return { | ||
ConditionalExpression(conditionalExpression) { | ||
const {test, consequent, alternate} = conditionalExpression; | ||
|
||
// `foo ? foo : bar` | ||
if (isSameNode(test, consequent, sourceCode)) { | ||
return getProblem({ | ||
sourceCode, | ||
conditionalExpression, | ||
left: test, | ||
right: alternate, | ||
}); | ||
} | ||
|
||
// `!bar ? foo : bar` | ||
if ( | ||
test.type === 'UnaryExpression' | ||
&& test.operator === '!' | ||
&& test.prefix | ||
&& isSameNode(test.argument, alternate, sourceCode) | ||
) { | ||
return getProblem({ | ||
sourceCode, | ||
conditionalExpression, | ||
left: test.argument, | ||
right: consequent, | ||
}); | ||
} | ||
}, | ||
}; | ||
}; | ||
|
||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
create, | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Prefer using a logical operator over a ternary.', | ||
}, | ||
|
||
hasSuggestions: true, | ||
messages, | ||
}, | ||
}; |
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,45 @@ | ||
import outdent from 'outdent'; | ||
import {getTester} from './utils/test.mjs'; | ||
|
||
const {test} = getTester(import.meta); | ||
|
||
test.snapshot({ | ||
valid: [ | ||
'foo ? foo1 : bar;', | ||
'foo.bar ? foo.bar1 : foo.baz', | ||
'foo.bar ? foo1.bar : foo.baz', | ||
'++foo ? ++foo : bar;', | ||
|
||
// Not checking | ||
'!!bar ? foo : bar;', | ||
], | ||
invalid: [ | ||
'foo ? foo : bar;', | ||
'foo.bar ? foo.bar : foo.baz', | ||
'foo?.bar ? foo.bar : baz', | ||
'!bar ? foo : bar;', | ||
'!!bar ? foo : !bar;', | ||
|
||
'foo() ? foo() : bar', | ||
|
||
// Children parentheses | ||
'foo ? foo : a && b', | ||
'foo ? foo : a || b', | ||
'foo ? foo : a ?? b', | ||
'a && b ? a && b : bar', | ||
'a || b ? a || b : bar', | ||
'a ?? b ? a ?? b : bar', | ||
'foo ? foo : await a', | ||
'await a ? await a : foo', | ||
|
||
// ASI | ||
outdent` | ||
const foo = [] | ||
!+a ? b : +a | ||
`, | ||
outdent` | ||
const foo = [] | ||
a && b ? a && b : 1 | ||
`, | ||
], | ||
}); |
Oops, something went wrong.