diff --git a/docs/rules/prefer-ternary.md b/docs/rules/prefer-ternary.md new file mode 100644 index 0000000000..67f916852b --- /dev/null +++ b/docs/rules/prefer-ternary.md @@ -0,0 +1,122 @@ +# Prefer ternary expressions over simple `if-else` statements + +This rule enforces the use of ternary expressions over 'simple' `if-else` statements, where 'simple' means the consequent and alternate are each one line and have the same basic type and form. + +Using an `if-else` statement typically results in more lines of code than a single-line ternary expression, which leads to an unnecessarily larger codebase that is more difficult to maintain. + +Additionally, using an `if-else` statement can result in defining variables using `let` or `var` solely to be reassigned within the blocks. This leads to variables being unnecessarily mutable and prevents `prefer-const` from flagging the variable. + +This rule is fixable. + +## Fail + +```js +function unicorn() { + if (test) { + return a; + } else { + return b; + } +} +``` + +```js +function* unicorn() { + if (test) { + yield a; + } else { + yield b; + } +} +``` + +```js +async function unicorn() { + if (test) { + await a(); + } else { + await b(); + } +} +``` + +```js +if (test) { + throw new Error('foo'); +} else { + throw new Error('bar'); +} +``` + +```js +let foo; +if (test) { + foo = 1; +} else { + foo = 2; +} +``` + +## Pass + +```js +function unicorn() { + return test ? a : b; +} +``` + +```js +function* unicorn() { + yield (test ? a : b); +} +``` + +```js +async function unicorn() { + await (test ? a() : b()); +} +``` + +```js +const error = test ? new Error('foo') : new Error('bar'); +throw error; +``` + +```js +let foo; +foo = test ? 1 : 2; +``` + +```js +// Multiple expressions +let foo; +let bar; +if (test) { + foo = 1; + bar = 2; +} else{ + foo = 2; +} +``` + +```js +// Different expressions +function unicorn() { + if (test) { + return a; + } else { + throw new Error('error'); + } +} +``` + +```js +// Assign to different variable +let foo; +let bar; +if (test) { + foo = 1; +} else{ + baz = 2; +} +``` diff --git a/index.js b/index.js index 21ef2f4797..0c0d5267f5 100644 --- a/index.js +++ b/index.js @@ -70,6 +70,7 @@ module.exports = { 'unicorn/prefer-spread': 'error', 'unicorn/prefer-starts-ends-with': 'error', 'unicorn/prefer-string-slice': 'error', + 'unicorn/prefer-ternary': 'error', 'unicorn/prefer-text-content': 'error', 'unicorn/prefer-trim-start-end': 'error', 'unicorn/prefer-type-error': 'error', diff --git a/package.json b/package.json index 2708a7416f..9c4259ce68 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,9 @@ "extends": [ "plugin:eslint-plugin/all" ], + "ignores": [ + "test/integration/{fixtures,unicorn}/**" + ], "overrides": [ { "files": "rules/utils/*.js", diff --git a/readme.md b/readme.md index 4d54fd49d1..8c99b58579 100644 --- a/readme.md +++ b/readme.md @@ -84,6 +84,7 @@ Configure it in `package.json`. "unicorn/prefer-spread": "error", "unicorn/prefer-starts-ends-with": "error", "unicorn/prefer-string-slice": "error", + "unicorn/prefer-ternary": "off", "unicorn/prefer-text-content": "error", "unicorn/prefer-trim-start-end": "error", "unicorn/prefer-type-error": "error", @@ -147,6 +148,7 @@ Configure it in `package.json`. - [prefer-spread](docs/rules/prefer-spread.md) - Prefer the spread operator over `Array.from()`. *(fixable)* - [prefer-starts-ends-with](docs/rules/prefer-starts-ends-with.md) - Prefer `String#startsWith()` & `String#endsWith()` over more complex alternatives. *(partly fixable)* - [prefer-string-slice](docs/rules/prefer-string-slice.md) - Prefer `String#slice()` over `String#substr()` and `String#substring()`. *(partly fixable)* +- [prefer-ternary](docs/rules/prefer-ternary.md) - Prefer ternary expressions over simple `if-else` statements. *(fixable)* - [prefer-text-content](docs/rules/prefer-text-content.md) - Prefer `.textContent` over `.innerText`. *(fixable)* - [prefer-trim-start-end](docs/rules/prefer-trim-start-end.md) - Prefer `String#trimStart()` / `String#trimEnd()` over `String#trimLeft()` / `String#trimRight()`. *(fixable)* - [prefer-type-error](docs/rules/prefer-type-error.md) - Enforce throwing `TypeError` in type checking conditions. *(fixable)* diff --git a/rules/no-for-loop.js b/rules/no-for-loop.js index 07393681f9..a4ec89363b 100644 --- a/rules/no-for-loop.js +++ b/rules/no-for-loop.js @@ -384,11 +384,9 @@ const create = context => { } if (elementNode) { - if (removeDeclaration) { - yield fixer.removeRange(getRemovalRange(elementNode, sourceCode)); - } else { - yield fixer.replaceText(elementNode.init, element); - } + yield removeDeclaration ? + fixer.removeRange(getRemovalRange(elementNode, sourceCode)) : + fixer.replaceText(elementNode.init, element); } }; } diff --git a/rules/prefer-ternary.js b/rules/prefer-ternary.js new file mode 100644 index 0000000000..350b4f3d22 --- /dev/null +++ b/rules/prefer-ternary.js @@ -0,0 +1,269 @@ +'use strict'; +const {isParenthesized} = require('eslint-utils'); +const {flatten} = require('lodash'); +const FixTracker = require('eslint/lib/rules/utils/fix-tracker'); +const getDocumentationUrl = require('./utils/get-documentation-url'); +const avoidCapture = require('./utils/avoid-capture'); + +const messageId = 'prefer-ternary'; + +const selector = [ + 'IfStatement', + ':not(IfStatement > IfStatement.alternate)', + '[test.type!="ConditionalExpression"]', + '[consequent]', + '[alternate]' +].join(''); + +const isTernary = node => node && node.type === 'ConditionalExpression'; + +function getNodeBody(node) { + /* istanbul ignore next */ + if (!node) { + return; + } + + if (node.type === 'ExpressionStatement') { + return getNodeBody(node.expression); + } + + if (node.type === 'BlockStatement') { + const body = node.body.filter(({type}) => type !== 'EmptyStatement'); + if (body.length === 1) { + return getNodeBody(body[0]); + } + } + + return node; +} + +function isSameAssignmentLeft(node1, node2) { + // [TODO]: Allow more types of left + return node1.type === node2.type && node1.type === 'Identifier' && node1.name === node2.name; +} + +const getIndentString = (node, sourceCode) => { + const {line, column} = sourceCode.getLocFromIndex(node.range[0]); + const lines = sourceCode.getLines(); + const before = lines[line - 1].slice(0, column); + + return before.match(/\s*$/)[0]; +}; + +const getScopes = scope => [ + scope, + ...flatten(scope.childScopes.map(scope => getScopes(scope))) +]; + +const create = context => { + const sourceCode = context.getSourceCode(); + const scopeToNamesGeneratedByFixer = new WeakMap(); + const isSafeName = (name, scopes) => scopes.every(scope => { + const generatedNames = scopeToNamesGeneratedByFixer.get(scope); + return !generatedNames || !generatedNames.has(name); + }); + + const getParenthesizedText = node => { + const text = sourceCode.getText(node); + return ( + isParenthesized(node, sourceCode) || + node.type === 'AwaitExpression' || + // Lower precedence, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table + node.type === 'AssignmentExpression' || + node.type === 'YieldExpression' || + node.type === 'SequenceExpression' + ) ? + `(${text})` : text; + }; + + function merge(options, mergeOptions) { + const { + before = '', + after = ';', + consequent, + alternate, + node + } = options; + + const { + checkThrowStatement, + returnFalseIfNotMergeable + } = { + checkThrowStatement: false, + returnFalseIfNotMergeable: false, + ...mergeOptions + }; + + if (!consequent || !alternate || consequent.type !== alternate.type) { + return returnFalseIfNotMergeable ? false : options; + } + + const {type} = consequent; + + if ( + type === 'ReturnStatement' && + !isTernary(consequent.argument) && + !isTernary(alternate.argument) + ) { + return merge({ + before: `${before}return `, + after, + consequent: consequent.argument === null ? 'undefined' : consequent.argument, + alternate: alternate.argument === null ? 'undefined' : alternate.argument, + node + }); + } + + if ( + type === 'YieldExpression' && + consequent.delegate === alternate.delegate && + !isTernary(consequent.argument) && + !isTernary(alternate.argument) + ) { + return merge({ + before: `${before}yield${consequent.delegate ? '*' : ''} (`, + after: `)${after}`, + consequent: consequent.argument === null ? 'undefined' : consequent.argument, + alternate: alternate.argument === null ? 'undefined' : alternate.argument, + node + }); + } + + if ( + type === 'AwaitExpression' && + !isTernary(consequent.argument) && + !isTernary(alternate.argument) + ) { + return merge({ + before: `${before}await (`, + after: `)${after}`, + consequent: consequent.argument, + alternate: alternate.argument, + node + }); + } + + if ( + checkThrowStatement && + type === 'ThrowStatement' && + !isTernary(consequent.argument) && + !isTernary(alternate.argument) + ) { + // `ThrowStatement` don't check nested + + // If `IfStatement` is not a `BlockStatement`, need add `{}` + const {parent} = node; + const needBraces = parent && parent.type !== 'BlockStatement'; + return { + type, + before: `${before}${needBraces ? '{\n{{INDENT_STRING}}' : ''}const {{ERROR_NAME}} = `, + after: `;\n{{INDENT_STRING}}throw {{ERROR_NAME}};${needBraces ? '\n}' : ''}`, + consequent: consequent.argument, + alternate: alternate.argument + }; + } + + if ( + type === 'AssignmentExpression' && + isSameAssignmentLeft(consequent.left, alternate.left) && + consequent.operator === alternate.operator && + !isTernary(consequent.left) && + !isTernary(alternate.left) && + !isTernary(consequent.right) && + !isTernary(alternate.right) + ) { + return merge({ + before: `${before}${sourceCode.getText(consequent.left)} ${consequent.operator} `, + after, + consequent: consequent.right, + alternate: alternate.right, + node + }); + } + + return returnFalseIfNotMergeable ? false : options; + } + + return { + [selector](node) { + const consequent = getNodeBody(node.consequent); + const alternate = getNodeBody(node.alternate); + + const result = merge({node, consequent, alternate}, { + checkThrowStatement: true, + returnFalseIfNotMergeable: true + }); + + if (!result) { + return; + } + + const scope = context.getScope(); + const sourceCode = context.getSourceCode(); + + context.report({ + node, + messageId, + fix: fixer => { + const testText = getParenthesizedText(node.test); + const consequentText = typeof result.consequent === 'string' ? + result.consequent : + getParenthesizedText(result.consequent); + const alternateText = typeof result.alternate === 'string' ? + result.alternate : + getParenthesizedText(result.alternate); + + let {type, before, after} = result; + + let generateNewVariables = false; + if (type === 'ThrowStatement') { + const scopes = getScopes(scope); + const errorName = avoidCapture('error', scopes, context.parserOptions.ecmaVersion, isSafeName); + + for (const scope of scopes) { + if (!scopeToNamesGeneratedByFixer.has(scope)) { + scopeToNamesGeneratedByFixer.set(scope, new Set()); + } + + const generatedNames = scopeToNamesGeneratedByFixer.get(scope); + generatedNames.add(errorName); + } + + const indentString = getIndentString(node, sourceCode); + + after = after + .replace('{{INDENT_STRING}}', indentString) + .replace('{{ERROR_NAME}}', errorName); + before = before + .replace('{{INDENT_STRING}}', indentString) + .replace('{{ERROR_NAME}}', errorName); + generateNewVariables = true; + } + + const fixed = `${before}${testText} ? ${consequentText} : ${alternateText}${after}`; + if (!generateNewVariables) { + return fixer.replaceText(node, fixed); + } + + return new FixTracker(fixer, sourceCode) + .retainRange(sourceCode.ast.range) + .replaceTextRange(node.range, fixed); + } + }); + } + }; +}; + +module.exports = { + create, + meta: { + type: 'suggestion', + docs: { + url: getDocumentationUrl(__filename) + }, + messages: { + [messageId]: 'This `if` statement can be replaced by a ternary expression.' + }, + fixable: 'code' + } +}; diff --git a/test/integration/projects.js b/test/integration/projects.js index 27d92a15ce..0be3fbad02 100644 --- a/test/integration/projects.js +++ b/test/integration/projects.js @@ -1,9 +1,14 @@ 'use strict'; +const path = require('path'); const typescriptArguments = ['--parser', '@typescript-eslint/parser', '--ext', '.ts,.js']; const vueArguments = ['--parser', 'vue-eslint-parser', '--ext', '.vue,.js']; module.exports = [ + { + name: 'unicorn', + location: path.join(__dirname, 'unicorn') + }, { repository: 'https://github.com/avajs/ava', extraArguments: [ diff --git a/test/integration/test.js b/test/integration/test.js index 2b091355b1..1fee098e0d 100755 --- a/test/integration/test.js +++ b/test/integration/test.js @@ -84,7 +84,7 @@ const makeEslintTask = (project, destination) => { const getBranch = mem(async dirname => (await execa('git', ['branch', '--show-current'], {cwd: dirname})).stdout); const execute = project => { - const destination = path.join(__dirname, 'fixtures', project.name); + const destination = project.location || path.join(__dirname, 'fixtures', project.name); return new Listr([ { @@ -152,10 +152,15 @@ list.run() if (error2.eslintMessage) { const {file, project, destination} = error2.eslintJob; const {line} = error2.eslintMessage; - // eslint-disable-next-line no-await-in-loop - const branch = await getBranch(destination); - console.error(chalk.gray(`${project.repository}/tree/${branch}/${path.relative(destination, file.filePath)}#L${line}`)); + if (project.repository) { + // eslint-disable-next-line no-await-in-loop + const branch = await getBranch(destination); + console.error(chalk.gray(`${project.repository}/blob/${branch}/${path.relative(destination, file.filePath)}#L${line}`)); + } else { + console.error(chalk.gray(`${path.relative(destination, file.filePath)}#L${line}`)); + } + console.error(chalk.gray(JSON.stringify(error2.eslintMessage, undefined, 2))); } } diff --git a/test/integration/unicorn/error-name-conflicts.js b/test/integration/unicorn/error-name-conflicts.js new file mode 100644 index 0000000000..f42ad9e0ae --- /dev/null +++ b/test/integration/unicorn/error-name-conflicts.js @@ -0,0 +1,12 @@ +function foo() { + try { + } catch (err) { + console.log(err); + + if (test) { + throw a; + } else { + throw b; + } + } +} diff --git a/test/lint/lint.js b/test/lint/lint.js index 009e956060..8530268523 100755 --- a/test/lint/lint.js +++ b/test/lint/lint.js @@ -17,7 +17,8 @@ const eslint = new ESLint({ overrideConfig: { ignorePatterns: [ 'coverage', - 'test/integration/fixtures' + 'test/integration/fixtures', + 'test/integration/unicorn' ], rules: { 'unicorn/prevent-abbreviations': [ diff --git a/test/prefer-ternary.js b/test/prefer-ternary.js new file mode 100644 index 0000000000..652dbf8015 --- /dev/null +++ b/test/prefer-ternary.js @@ -0,0 +1,1183 @@ +import test from 'ava'; +import avaRuleTester from 'eslint-ava-rule-tester'; +import {outdent} from 'outdent'; +import rule from '../rules/prefer-ternary'; + +const messageId = 'prefer-ternary'; + +const ruleTester = avaRuleTester(test, { + parserOptions: { + ecmaVersion: 2020 + } +}); + +const babelRuleTester = avaRuleTester(test, { + parser: require.resolve('babel-eslint') +}); + +const errors = [{messageId}]; + +// ReturnStatement +ruleTester.run('prefer-ternary', rule, { + valid: [ + // Test is Ternary + outdent` + function unicorn() { + if(a ? b : c){ + return a; + } else{ + return b; + } + } + `, + // Consequent is Ternary + outdent` + function unicorn() { + if(test){ + return a ? b : c; + } else{ + return b; + } + } + `, + // Alternate is Ternary + outdent` + function unicorn() { + if(test){ + return a; + } else{ + return a ? b : c; + } + } + ` + ], + invalid: [ + { + code: outdent` + function unicorn() { + if(test){ + return a; + } else{ + return b; + } + } + `, + output: outdent` + function unicorn() { + return test ? a : b; + } + `, + errors + }, + { + code: outdent` + async function unicorn() { + if(test){ + return await a; + } else{ + return b; + } + } + `, + output: outdent` + async function unicorn() { + return test ? (await a) : b; + } + `, + errors + }, + { + code: outdent` + async function unicorn() { + if(test){ + return await a; + } else{ + return await b; + } + } + `, + output: outdent` + async function unicorn() { + return await (test ? a : b); + } + `, + errors + }, + { + code: outdent` + function unicorn() { + if(test){ + return; + } else{ + return b; + } + } + `, + output: outdent` + function unicorn() { + return test ? undefined : b; + } + `, + errors + }, + { + code: outdent` + function unicorn() { + if(test){ + return; + } else{ + return; + } + } + `, + output: outdent` + function unicorn() { + return test ? undefined : undefined; + } + `, + errors + }, + { + code: outdent` + async function unicorn() { + if(test){ + return; + } else{ + return await b; + } + } + `, + output: outdent` + async function unicorn() { + return test ? undefined : (await b); + } + `, + errors + }, + // Crazy nested + { + code: outdent` + async function* unicorn() { + if(test){ + return yield await (foo = a); + } else{ + return yield await (foo = b); + } + } + `, + output: outdent` + async function* unicorn() { + return yield (await (foo = test ? a : b)); + } + `, + errors + } + ] +}); + +// YieldExpression +ruleTester.run('prefer-ternary', rule, { + valid: [ + // Different `delegate` + outdent` + function* unicorn() { + if(test){ + yield* a; + } else{ + yield b; + } + } + `, + // Test is Ternary + outdent` + function* unicorn() { + if(a ? b : c){ + yield a; + } else{ + yield b; + } + } + `, + // Consequent is Ternary + outdent` + function* unicorn() { + if(test){ + yield a ? b : c; + } else{ + yield b; + } + } + `, + // Alternate is Ternary + outdent` + function* unicorn() { + if(test){ + yield a; + } else{ + yield a ? b : c; + } + } + ` + ], + invalid: [ + { + code: outdent` + function* unicorn() { + if(test){ + yield a; + } else{ + yield b; + } + } + `, + output: outdent` + function* unicorn() { + yield (test ? a : b); + } + `, + errors + }, + { + code: outdent` + function* unicorn() { + if(test){ + yield; + } else{ + yield b; + } + } + `, + output: outdent` + function* unicorn() { + yield (test ? undefined : b); + } + `, + errors + }, + { + code: outdent` + function* unicorn() { + if(test){ + yield; + } else{ + yield; + } + } + `, + output: outdent` + function* unicorn() { + yield (test ? undefined : undefined); + } + `, + errors + }, + { + code: outdent` + async function* unicorn() { + if(test){ + yield; + } else{ + yield await b; + } + } + `, + output: outdent` + async function* unicorn() { + yield (test ? undefined : (await b)); + } + `, + errors + }, + { + code: outdent` + function* unicorn() { + if(test){ + yield* a; + } else{ + yield* b; + } + } + `, + output: outdent` + function* unicorn() { + yield* (test ? a : b); + } + `, + errors + }, + { + code: outdent` + async function* unicorn() { + if(test){ + yield await a; + } else{ + yield b; + } + } + `, + output: outdent` + async function* unicorn() { + yield (test ? (await a) : b); + } + `, + errors + }, + { + code: outdent` + async function* unicorn() { + if(test){ + yield await a; + } else{ + yield await b; + } + } + `, + output: outdent` + async function* unicorn() { + yield (await (test ? a : b)); + } + `, + errors + } + ] +}); + +// AwaitExpression +ruleTester.run('prefer-ternary', rule, { + valid: [ + // Test is Ternary + outdent` + async function unicorn() { + if(a ? b : c){ + await a; + } else{ + await b; + } + } + `, + // Consequent is Ternary + outdent` + async function unicorn() { + if(test){ + await a ? b : c; + } else{ + await b; + } + } + `, + // Alternate is Ternary + outdent` + async function unicorn() { + if(test){ + await a; + } else{ + await a ? b : c; + } + } + ` + ], + invalid: [ + { + code: outdent` + async function unicorn() { + if(test){ + await doSomething1(); + } else{ + await doSomething2(); + } + } + `, + output: outdent` + async function unicorn() { + await (test ? doSomething1() : doSomething2()); + } + `, + errors + }, + { + code: outdent` + async function unicorn() { + if(test){ + await a; + } else{ + await b; + } + } + `, + output: outdent` + async function unicorn() { + await (test ? a : b); + } + `, + errors + } + ] +}); + +// ThrowStatement +ruleTester.run('prefer-ternary', rule, { + valid: [ + // Test is Ternary + outdent` + function unicorn() { + if(a ? b : c){ + throw a; + } else{ + throw b; + } + } + `, + // Consequent is Ternary + outdent` + function unicorn() { + if (test) { + throw a ? b : c; + } else { + throw b; + } + } + `, + // Alternate is Ternary + outdent` + function unicorn() { + if (test) { + throw a; + } else { + throw a ? b : c; + } + } + ` + ], + invalid: [ + { + code: outdent` + function unicorn() { + if (test) { + throw new Error('a'); + } else{ + throw new TypeError('a'); + } + } + `, + output: outdent` + function unicorn() { + const error = test ? new Error('a') : new TypeError('a'); + throw error; + } + `, + errors + }, + { + code: outdent` + function unicorn() { + if (test) { + throw a; + } else { + throw b; + } + } + `, + output: outdent` + function unicorn() { + const error = test ? a : b; + throw error; + } + `, + errors + }, + // Indention + { + code: outdent` + function unicorn() { + /* comment cause wrong indention */ if (test) { + throw a; + } else { + throw b; + } + } + `, + output: outdent` + function unicorn() { + /* comment cause wrong indention */ const error = test ? a : b; + throw error; + } + `, + errors + }, + { + code: outdent` + function unicorn() { + if (test) { + throw a; + } else { + throw b; + } + } + `, + output: outdent` + function unicorn() { + const error = test ? a : b; + throw error; + } + `, + errors + }, + // Space + { + code: outdent` + function unicorn() { + if (test) { + throw new Error('a'); + } else { + throw new TypeError('a'); + } + } + `.replace(/\t/g, ' '), + output: outdent` + function unicorn() { + const error = test ? new Error('a') : new TypeError('a'); + throw error; + } + `.replace(/\t/g, ' '), + errors + }, + { + code: outdent` + async function unicorn() { + if (test) { + throw await a; + } else { + throw b; + } + } + `, + output: outdent` + async function unicorn() { + const error = test ? (await a) : b; + throw error; + } + `, + errors + }, + // `ThrowStatement` don't check nested + { + code: outdent` + async function unicorn() { + if (test) { + throw await a; + } else { + throw await b; + } + } + `, + output: outdent` + async function unicorn() { + const error = test ? (await a) : (await b); + throw error; + } + `, + errors + }, + // `error` is used + { + code: outdent` + function unicorn() { + const error = new Error(); + if (test) { + throw a; + } else { + throw b; + } + } + `, + output: outdent` + function unicorn() { + const error = new Error(); + const error_ = test ? a : b; + throw error_; + } + `, + errors + }, + // Child scope + { + code: outdent` + function unicorn() { + if (test) { + throw a; + } else { + throw b; + } + + try {} catch(error) { + const error_ = new TypeError(error); + throw error_; + } + } + `, + output: outdent` + function unicorn() { + const error__ = test ? a : b; + throw error__; + + try {} catch(error) { + const error_ = new TypeError(error); + throw error_; + } + } + `, + errors + }, + // Global + { + code: outdent` + function unicorn() { + if (test) { + throw a; + } else { + throw b; + } + + function foo() { + throw error; + } + } + `, + output: outdent` + function unicorn() { + const error_ = test ? a : b; + throw error_; + + function foo() { + throw error; + } + } + `, + errors + }, + // Multiple + // This will fix one by one, see next test + { + code: outdent` + function unicorn() { + if (test) { + throw a; + } else { + throw b; + } + + if (test) { + throw a; + } else { + throw b; + } + } + `, + output: outdent` + function unicorn() { + const error = test ? a : b; + throw error; + + if (test) { + throw a; + } else { + throw b; + } + } + `, + errors: [...errors, ...errors] + }, + // This `code` is `output` from previous test + { + code: outdent` + function unicorn() { + const error = test ? a : b; + throw error; + + if (test) { + throw a; + } else { + throw b; + } + } + `, + output: outdent` + function unicorn() { + const error = test ? a : b; + throw error; + + const error_ = test ? a : b; + throw error_; + } + `, + errors + }, + // Multiple nested + // This will fix one by one, see next test + { + code: outdent` + function outer() { + if (test) { + throw a; + } else { + throw b; + } + + function inner() { + if (test) { + throw a; + } else { + throw b; + } + } + } + `, + output: outdent` + function outer() { + const error = test ? a : b; + throw error; + + function inner() { + if (test) { + throw a; + } else { + throw b; + } + } + } + `, + errors: [...errors, ...errors] + }, + // This `code` is `output` from previous test + { + code: outdent` + function outer() { + const error = test ? a : b; + throw error; + + function inner() { + if (test) { + throw a; + } else { + throw b; + } + } + } + `, + output: outdent` + function outer() { + const error = test ? a : b; + throw error; + + function inner() { + const error_ = test ? a : b; + throw error_; + } + } + `, + errors + }, + // Need `{}` + { + code: outdent` + while (foo) if (test) {throw a} else {throw b} + `, + output: outdent` + while (foo) { + const error = test ? a : b; + throw error; + } + `, + errors + } + ] +}); + +// AssignmentExpression +ruleTester.run('prefer-ternary', rule, { + valid: [ + // Different `left` + outdent` + function unicorn() { + if(test){ + foo = a; + } else{ + bar = b; + } + } + `, + // Different `operator` + outdent` + function unicorn() { + if(test){ + foo = a; + } else{ + foo *= b; + } + } + `, + // Same `left`, but not handled + outdent` + function unicorn() { + if(test){ + foo.bar = a; + } else{ + foo.bar = b; + } + } + `, + // Test is Ternary + outdent` + function unicorn() { + if(a ? b : c){ + foo = a; + } else{ + foo = b; + } + } + `, + // Consequent is Ternary + outdent` + function unicorn() { + if(test){ + foo = a ? b : c; + } else{ + foo = b; + } + } + `, + // Alternate is Ternary + outdent` + function unicorn() { + if(test){ + foo = a; + } else{ + foo = a ? b : c; + } + } + ` + ], + invalid: [ + { + code: outdent` + function unicorn() { + if(test){ + foo = a; + } else{ + foo = b; + } + } + `, + output: outdent` + function unicorn() { + foo = test ? a : b; + } + `, + errors + }, + { + code: outdent` + function unicorn() { + if(test){ + foo *= a; + } else{ + foo *= b; + } + } + `, + output: outdent` + function unicorn() { + foo *= test ? a : b; + } + `, + errors + }, + { + code: outdent` + async function unicorn() { + if(test){ + foo = await a; + } else{ + foo = b; + } + } + `, + output: outdent` + async function unicorn() { + foo = test ? (await a) : b; + } + `, + errors + }, + { + code: outdent` + async function unicorn() { + if(test){ + foo = await a; + } else{ + foo = await b; + } + } + `, + output: outdent` + async function unicorn() { + foo = await (test ? a : b); + } + `, + errors + }, + // Crazy nested + { + code: outdent` + async function* unicorn() { + if(test){ + foo = yield await a; + } else{ + foo = yield await b; + } + } + `, + output: outdent` + async function* unicorn() { + foo = yield (await (test ? a : b)); + } + `, + errors + }, + { + code: outdent` + if(test){ + $0 |= $1 ^= $2 &= $3 >>>= $4 >>= $5 <<= $6 %= $7 /= $8 *= $9 **= $10 -= $11 += $12 = + _STOP_ = + $0 |= $1 ^= $2 &= $3 >>>= $4 >>= $5 <<= $6 %= $7 /= $8 *= $9 **= $10 -= $11 += $12 = + 1; + } else{ + $0 |= $1 ^= $2 &= $3 >>>= $4 >>= $5 <<= $6 %= $7 /= $8 *= $9 **= $10 -= $11 += $12 = + _STOP_2_ = + $0 |= $1 ^= $2 &= $3 >>>= $4 >>= $5 <<= $6 %= $7 /= $8 *= $9 **= $10 -= $11 += $12 = + 2; + } + `, + output: outdent` + $0 |= $1 ^= $2 &= $3 >>>= $4 >>= $5 <<= $6 %= $7 /= $8 *= $9 **= $10 -= $11 += $12 = test ? (_STOP_ = + $0 |= $1 ^= $2 &= $3 >>>= $4 >>= $5 <<= $6 %= $7 /= $8 *= $9 **= $10 -= $11 += $12 = + 1) : (_STOP_2_ = + $0 |= $1 ^= $2 &= $3 >>>= $4 >>= $5 <<= $6 %= $7 /= $8 *= $9 **= $10 -= $11 += $12 = + 2); + `, + errors + } + ] +}); + +ruleTester.run('prefer-ternary', rule, { + valid: [ + // No `consequent` / `alternate` + 'if (a) {b}', + 'if (a) {} else {b}', + 'if (a) {} else {}', + + // Call is not allow to merge + outdent` + if (test) { + a(); + } else { + b(); + } + `, + + // + outdent` + function foo(){ + if (a) { + return 1; + } else if (b) { + return 2; + } else if (c) { + return 3; + } else { + return 4; + } + } + ` + ], + invalid: [ + // Empty block should not matters + { + code: outdent` + function unicorn() { + if (test) { + ; // Empty block + return a; + } else { + return b; + } + } + `, + output: outdent` + function unicorn() { + return test ? a : b; + } + `, + errors + }, + // `ExpressionStatement` or `BlockStatement` should not matters + { + code: outdent` + function unicorn() { + if (test) { + foo = a + } else foo = b; + } + `, + output: outdent` + function unicorn() { + foo = test ? a : b; + } + `, + errors + }, + // No `ExpressionStatement` or `BlockStatement` should not matters + { + code: outdent` + function unicorn() { + if (test) return a; + else return b; + } + `, + output: outdent` + function unicorn() { + return test ? a : b; + } + `, + errors + }, + { + code: outdent` + function unicorn() { + if (test) return a; + else return b; + } + `, + output: outdent` + function unicorn() { + return test ? a : b; + } + `, + errors + }, + + // Precedence + { + code: outdent` + if (a = b) { + foo = 1; + } else foo = 2; + `, + output: 'foo = (a = b) ? 1 : 2;', + errors + }, + { + code: outdent` + function* unicorn() { + if (yield a) { + foo = 1; + } else foo = 2; + } + `, + output: outdent` + function* unicorn() { + foo = (yield a) ? 1 : 2; + } + `, + errors + }, + { + code: outdent` + function* unicorn() { + if (yield* a) { + foo = 1; + } else foo = 2; + } + `, + output: outdent` + function* unicorn() { + foo = (yield* a) ? 1 : 2; + } + `, + errors + }, + + // Nested + { + code: outdent` + function foo(){ + if (a) { + return 1; + } else { + if (b) { + return 2; + } else { + return 3; + } + } + } + `, + output: outdent` + function foo(){ + if (a) { + return 1; + } else { + return b ? 2 : 3; + } + } + `, + errors + }, + { + code: outdent` + function foo(){ + if (a) { + if (b) { + return 1; + } else { + return 2; + } + } else { + return 3; + } + } + `, + output: outdent` + function foo(){ + if (a) { + return b ? 1 : 2; + } else { + return 3; + } + } + `, + errors + } + ] +}); + +babelRuleTester.run('prefer-ternary', rule, { + valid: [], + invalid: [ + // https://github.com/facebook/react/blob/7a1691cdff209249b49a4472ba87b542980a5f71/packages/react-dom/src/client/DOMPropertyOperations.js#L183 + { + code: outdent` + if (enableTrustedTypesIntegration) { + attributeValue = (value: any); + } else { + attributeValue = '' + (value: any); + } + `, + output: outdent` + attributeValue = enableTrustedTypesIntegration ? (value: any) : '' + (value: any); + `, + errors + } + ] +});