prefer-ternary.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. 'use strict';
  2. const {isParenthesized} = require('@eslint-community/eslint-utils');
  3. const avoidCapture = require('./utils/avoid-capture.js');
  4. const needsSemicolon = require('./utils/needs-semicolon.js');
  5. const isSameReference = require('./utils/is-same-reference.js');
  6. const getIndentString = require('./utils/get-indent-string.js');
  7. const {getParenthesizedText} = require('./utils/parentheses.js');
  8. const shouldAddParenthesesToConditionalExpressionChild = require('./utils/should-add-parentheses-to-conditional-expression-child.js');
  9. const {extendFixRange} = require('./fix/index.js');
  10. const getScopes = require('./utils/get-scopes.js');
  11. const messageId = 'prefer-ternary';
  12. const selector = [
  13. 'IfStatement',
  14. ':not(IfStatement > .alternate)',
  15. '[test.type!="ConditionalExpression"]',
  16. '[consequent]',
  17. '[alternate]',
  18. ].join('');
  19. const isTernary = node => node?.type === 'ConditionalExpression';
  20. function getNodeBody(node) {
  21. /* c8 ignore next 3 */
  22. if (!node) {
  23. return;
  24. }
  25. if (node.type === 'ExpressionStatement') {
  26. return getNodeBody(node.expression);
  27. }
  28. if (node.type === 'BlockStatement') {
  29. const body = node.body.filter(({type}) => type !== 'EmptyStatement');
  30. if (body.length === 1) {
  31. return getNodeBody(body[0]);
  32. }
  33. }
  34. return node;
  35. }
  36. const isSingleLineNode = node => node.loc.start.line === node.loc.end.line;
  37. /** @param {import('eslint').Rule.RuleContext} context */
  38. const create = context => {
  39. const onlySingleLine = context.options[0] === 'only-single-line';
  40. const sourceCode = context.getSourceCode();
  41. const scopeToNamesGeneratedByFixer = new WeakMap();
  42. const isSafeName = (name, scopes) => scopes.every(scope => {
  43. const generatedNames = scopeToNamesGeneratedByFixer.get(scope);
  44. return !generatedNames || !generatedNames.has(name);
  45. });
  46. const getText = node => {
  47. let text = getParenthesizedText(node, sourceCode);
  48. if (
  49. !isParenthesized(node, sourceCode)
  50. && shouldAddParenthesesToConditionalExpressionChild(node)
  51. ) {
  52. text = `(${text})`;
  53. }
  54. return text;
  55. };
  56. function merge(options, mergeOptions) {
  57. const {
  58. before = '',
  59. after = ';',
  60. consequent,
  61. alternate,
  62. node,
  63. } = options;
  64. const {
  65. checkThrowStatement,
  66. returnFalseIfNotMergeable,
  67. } = {
  68. checkThrowStatement: false,
  69. returnFalseIfNotMergeable: false,
  70. ...mergeOptions,
  71. };
  72. if (!consequent || !alternate || consequent.type !== alternate.type) {
  73. return returnFalseIfNotMergeable ? false : options;
  74. }
  75. const {type, argument, delegate, left, right, operator} = consequent;
  76. if (
  77. type === 'ReturnStatement'
  78. && !isTernary(argument)
  79. && !isTernary(alternate.argument)
  80. ) {
  81. return merge({
  82. before: `${before}return `,
  83. after,
  84. consequent: argument === null ? 'undefined' : argument,
  85. alternate: alternate.argument === null ? 'undefined' : alternate.argument,
  86. node,
  87. });
  88. }
  89. if (
  90. type === 'YieldExpression'
  91. && delegate === alternate.delegate
  92. && !isTernary(argument)
  93. && !isTernary(alternate.argument)
  94. ) {
  95. return merge({
  96. before: `${before}yield${delegate ? '*' : ''} (`,
  97. after: `)${after}`,
  98. consequent: argument === null ? 'undefined' : argument,
  99. alternate: alternate.argument === null ? 'undefined' : alternate.argument,
  100. node,
  101. });
  102. }
  103. if (
  104. type === 'AwaitExpression'
  105. && !isTernary(argument)
  106. && !isTernary(alternate.argument)
  107. ) {
  108. return merge({
  109. before: `${before}await (`,
  110. after: `)${after}`,
  111. consequent: argument,
  112. alternate: alternate.argument,
  113. node,
  114. });
  115. }
  116. if (
  117. checkThrowStatement
  118. && type === 'ThrowStatement'
  119. && !isTernary(argument)
  120. && !isTernary(alternate.argument)
  121. ) {
  122. // `ThrowStatement` don't check nested
  123. // If `IfStatement` is not a `BlockStatement`, need add `{}`
  124. const {parent} = node;
  125. const needBraces = parent && parent.type !== 'BlockStatement';
  126. return {
  127. type,
  128. before: `${before}${needBraces ? '{\n{{INDENT_STRING}}' : ''}const {{ERROR_NAME}} = `,
  129. after: `;\n{{INDENT_STRING}}throw {{ERROR_NAME}};${needBraces ? '\n}' : ''}`,
  130. consequent: argument,
  131. alternate: alternate.argument,
  132. };
  133. }
  134. if (
  135. type === 'AssignmentExpression'
  136. && operator === alternate.operator
  137. && !isTernary(left)
  138. && !isTernary(alternate.left)
  139. && !isTernary(right)
  140. && !isTernary(alternate.right)
  141. && isSameReference(left, alternate.left)
  142. ) {
  143. return merge({
  144. before: `${before}${sourceCode.getText(left)} ${operator} `,
  145. after,
  146. consequent: right,
  147. alternate: alternate.right,
  148. node,
  149. });
  150. }
  151. return returnFalseIfNotMergeable ? false : options;
  152. }
  153. return {
  154. [selector](node) {
  155. const consequent = getNodeBody(node.consequent);
  156. const alternate = getNodeBody(node.alternate);
  157. if (
  158. onlySingleLine
  159. && [consequent, alternate, node.test].some(node => !isSingleLineNode(node))
  160. ) {
  161. return;
  162. }
  163. const result = merge({node, consequent, alternate}, {
  164. checkThrowStatement: true,
  165. returnFalseIfNotMergeable: true,
  166. });
  167. if (!result) {
  168. return;
  169. }
  170. const problem = {node, messageId};
  171. // Don't fix if there are comments
  172. if (sourceCode.getCommentsInside(node).length > 0) {
  173. return problem;
  174. }
  175. const scope = context.getScope();
  176. problem.fix = function * (fixer) {
  177. const testText = getText(node.test);
  178. const consequentText = typeof result.consequent === 'string'
  179. ? result.consequent
  180. : getText(result.consequent);
  181. const alternateText = typeof result.alternate === 'string'
  182. ? result.alternate
  183. : getText(result.alternate);
  184. let {type, before, after} = result;
  185. let generateNewVariables = false;
  186. if (type === 'ThrowStatement') {
  187. const scopes = getScopes(scope);
  188. const errorName = avoidCapture('error', scopes, isSafeName);
  189. for (const scope of scopes) {
  190. if (!scopeToNamesGeneratedByFixer.has(scope)) {
  191. scopeToNamesGeneratedByFixer.set(scope, new Set());
  192. }
  193. const generatedNames = scopeToNamesGeneratedByFixer.get(scope);
  194. generatedNames.add(errorName);
  195. }
  196. const indentString = getIndentString(node, sourceCode);
  197. after = after
  198. .replace('{{INDENT_STRING}}', indentString)
  199. .replace('{{ERROR_NAME}}', errorName);
  200. before = before
  201. .replace('{{INDENT_STRING}}', indentString)
  202. .replace('{{ERROR_NAME}}', errorName);
  203. generateNewVariables = true;
  204. }
  205. let fixed = `${before}${testText} ? ${consequentText} : ${alternateText}${after}`;
  206. const tokenBefore = sourceCode.getTokenBefore(node);
  207. const shouldAddSemicolonBefore = needsSemicolon(tokenBefore, sourceCode, fixed);
  208. if (shouldAddSemicolonBefore) {
  209. fixed = `;${fixed}`;
  210. }
  211. yield fixer.replaceText(node, fixed);
  212. if (generateNewVariables) {
  213. yield * extendFixRange(fixer, sourceCode.ast.range);
  214. }
  215. };
  216. return problem;
  217. },
  218. };
  219. };
  220. const schema = [
  221. {
  222. enum: ['always', 'only-single-line'],
  223. default: 'always',
  224. },
  225. ];
  226. /** @type {import('eslint').Rule.RuleModule} */
  227. module.exports = {
  228. create,
  229. meta: {
  230. type: 'suggestion',
  231. docs: {
  232. description: 'Prefer ternary expressions over simple `if-else` statements.',
  233. },
  234. fixable: 'code',
  235. schema,
  236. messages: {
  237. [messageId]: 'This `if` statement can be replaced by a ternary expression.',
  238. },
  239. },
  240. };