123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- 'use strict';
- const {isParenthesized} = require('@eslint-community/eslint-utils');
- const avoidCapture = require('./utils/avoid-capture.js');
- const needsSemicolon = require('./utils/needs-semicolon.js');
- const isSameReference = require('./utils/is-same-reference.js');
- const getIndentString = require('./utils/get-indent-string.js');
- const {getParenthesizedText} = require('./utils/parentheses.js');
- const shouldAddParenthesesToConditionalExpressionChild = require('./utils/should-add-parentheses-to-conditional-expression-child.js');
- const {extendFixRange} = require('./fix/index.js');
- const getScopes = require('./utils/get-scopes.js');
- const messageId = 'prefer-ternary';
- const selector = [
- 'IfStatement',
- ':not(IfStatement > .alternate)',
- '[test.type!="ConditionalExpression"]',
- '[consequent]',
- '[alternate]',
- ].join('');
- const isTernary = node => node?.type === 'ConditionalExpression';
- function getNodeBody(node) {
- /* c8 ignore next 3 */
- 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;
- }
- const isSingleLineNode = node => node.loc.start.line === node.loc.end.line;
- /** @param {import('eslint').Rule.RuleContext} context */
- const create = context => {
- const onlySingleLine = context.options[0] === 'only-single-line';
- 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 getText = node => {
- let text = getParenthesizedText(node, sourceCode);
- if (
- !isParenthesized(node, sourceCode)
- && shouldAddParenthesesToConditionalExpressionChild(node)
- ) {
- text = `(${text})`;
- }
- return 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, argument, delegate, left, right, operator} = consequent;
- if (
- type === 'ReturnStatement'
- && !isTernary(argument)
- && !isTernary(alternate.argument)
- ) {
- return merge({
- before: `${before}return `,
- after,
- consequent: argument === null ? 'undefined' : argument,
- alternate: alternate.argument === null ? 'undefined' : alternate.argument,
- node,
- });
- }
- if (
- type === 'YieldExpression'
- && delegate === alternate.delegate
- && !isTernary(argument)
- && !isTernary(alternate.argument)
- ) {
- return merge({
- before: `${before}yield${delegate ? '*' : ''} (`,
- after: `)${after}`,
- consequent: argument === null ? 'undefined' : argument,
- alternate: alternate.argument === null ? 'undefined' : alternate.argument,
- node,
- });
- }
- if (
- type === 'AwaitExpression'
- && !isTernary(argument)
- && !isTernary(alternate.argument)
- ) {
- return merge({
- before: `${before}await (`,
- after: `)${after}`,
- consequent: argument,
- alternate: alternate.argument,
- node,
- });
- }
- if (
- checkThrowStatement
- && type === 'ThrowStatement'
- && !isTernary(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: argument,
- alternate: alternate.argument,
- };
- }
- if (
- type === 'AssignmentExpression'
- && operator === alternate.operator
- && !isTernary(left)
- && !isTernary(alternate.left)
- && !isTernary(right)
- && !isTernary(alternate.right)
- && isSameReference(left, alternate.left)
- ) {
- return merge({
- before: `${before}${sourceCode.getText(left)} ${operator} `,
- after,
- consequent: right,
- alternate: alternate.right,
- node,
- });
- }
- return returnFalseIfNotMergeable ? false : options;
- }
- return {
- [selector](node) {
- const consequent = getNodeBody(node.consequent);
- const alternate = getNodeBody(node.alternate);
- if (
- onlySingleLine
- && [consequent, alternate, node.test].some(node => !isSingleLineNode(node))
- ) {
- return;
- }
- const result = merge({node, consequent, alternate}, {
- checkThrowStatement: true,
- returnFalseIfNotMergeable: true,
- });
- if (!result) {
- return;
- }
- const problem = {node, messageId};
- // Don't fix if there are comments
- if (sourceCode.getCommentsInside(node).length > 0) {
- return problem;
- }
- const scope = context.getScope();
- problem.fix = function * (fixer) {
- const testText = getText(node.test);
- const consequentText = typeof result.consequent === 'string'
- ? result.consequent
- : getText(result.consequent);
- const alternateText = typeof result.alternate === 'string'
- ? result.alternate
- : getText(result.alternate);
- let {type, before, after} = result;
- let generateNewVariables = false;
- if (type === 'ThrowStatement') {
- const scopes = getScopes(scope);
- const errorName = avoidCapture('error', scopes, 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;
- }
- let fixed = `${before}${testText} ? ${consequentText} : ${alternateText}${after}`;
- const tokenBefore = sourceCode.getTokenBefore(node);
- const shouldAddSemicolonBefore = needsSemicolon(tokenBefore, sourceCode, fixed);
- if (shouldAddSemicolonBefore) {
- fixed = `;${fixed}`;
- }
- yield fixer.replaceText(node, fixed);
- if (generateNewVariables) {
- yield * extendFixRange(fixer, sourceCode.ast.range);
- }
- };
- return problem;
- },
- };
- };
- const schema = [
- {
- enum: ['always', 'only-single-line'],
- default: 'always',
- },
- ];
- /** @type {import('eslint').Rule.RuleModule} */
- module.exports = {
- create,
- meta: {
- type: 'suggestion',
- docs: {
- description: 'Prefer ternary expressions over simple `if-else` statements.',
- },
- fixable: 'code',
- schema,
- messages: {
- [messageId]: 'This `if` statement can be replaced by a ternary expression.',
- },
- },
- };
|