prefer-default-parameters.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. 'use strict';
  2. const {findVariable} = require('@eslint-community/eslint-utils');
  3. const MESSAGE_ID = 'preferDefaultParameters';
  4. const MESSAGE_ID_SUGGEST = 'preferDefaultParametersSuggest';
  5. const assignmentSelector = [
  6. 'ExpressionStatement',
  7. '[expression.type="AssignmentExpression"]',
  8. ].join('');
  9. const declarationSelector = [
  10. 'VariableDeclaration',
  11. '[declarations.0.type="VariableDeclarator"]',
  12. ].join('');
  13. const isDefaultExpression = (left, right) =>
  14. left
  15. && right
  16. && left.type === 'Identifier'
  17. && right.type === 'LogicalExpression'
  18. && (right.operator === '||' || right.operator === '??')
  19. && right.left.type === 'Identifier'
  20. && right.right.type === 'Literal';
  21. const containsCallExpression = (sourceCode, node) => {
  22. if (!node) {
  23. return false;
  24. }
  25. if (node.type === 'CallExpression') {
  26. return true;
  27. }
  28. const keys = sourceCode.visitorKeys[node.type];
  29. for (const key of keys) {
  30. const value = node[key];
  31. if (Array.isArray(value)) {
  32. for (const element of value) {
  33. if (containsCallExpression(sourceCode, element)) {
  34. return true;
  35. }
  36. }
  37. } else if (containsCallExpression(sourceCode, value)) {
  38. return true;
  39. }
  40. }
  41. return false;
  42. };
  43. const hasSideEffects = (sourceCode, function_, node) => {
  44. for (const element of function_.body.body) {
  45. if (element === node) {
  46. break;
  47. }
  48. // Function call before default-assignment
  49. if (containsCallExpression(sourceCode, element)) {
  50. return true;
  51. }
  52. }
  53. return false;
  54. };
  55. const hasExtraReferences = (assignment, references, left) => {
  56. // Parameter is referenced prior to default-assignment
  57. if (assignment && references[0].identifier !== left) {
  58. return true;
  59. }
  60. // Old parameter is still referenced somewhere else
  61. if (!assignment && references.length > 1) {
  62. return true;
  63. }
  64. return false;
  65. };
  66. const isLastParameter = (parameters, parameter) => {
  67. const lastParameter = parameters[parameters.length - 1];
  68. // See 'default-param-last' rule
  69. return parameter && parameter === lastParameter;
  70. };
  71. const needsParentheses = (sourceCode, function_) => {
  72. if (function_.type !== 'ArrowFunctionExpression' || function_.params.length > 1) {
  73. return false;
  74. }
  75. const [parameter] = function_.params;
  76. const before = sourceCode.getTokenBefore(parameter);
  77. const after = sourceCode.getTokenAfter(parameter);
  78. return !after || !before || before.value !== '(' || after.value !== ')';
  79. };
  80. /** @param {import('eslint').Rule.RuleFixer} fixer */
  81. const fixDefaultExpression = (fixer, sourceCode, node) => {
  82. const {line} = node.loc.start;
  83. const {column} = node.loc.end;
  84. const nodeText = sourceCode.getText(node);
  85. const lineText = sourceCode.lines[line - 1];
  86. const isOnlyNodeOnLine = lineText.trim() === nodeText;
  87. const endsWithWhitespace = lineText[column] === ' ';
  88. if (isOnlyNodeOnLine) {
  89. return fixer.removeRange([
  90. sourceCode.getIndexFromLoc({line, column: 0}),
  91. sourceCode.getIndexFromLoc({line: line + 1, column: 0}),
  92. ]);
  93. }
  94. if (endsWithWhitespace) {
  95. return fixer.removeRange([
  96. node.range[0],
  97. node.range[1] + 1,
  98. ]);
  99. }
  100. return fixer.remove(node);
  101. };
  102. /** @param {import('eslint').Rule.RuleContext} context */
  103. const create = context => {
  104. const sourceCode = context.getSourceCode();
  105. const functionStack = [];
  106. const checkExpression = (node, left, right, assignment) => {
  107. const currentFunction = functionStack[functionStack.length - 1];
  108. if (!currentFunction || !isDefaultExpression(left, right)) {
  109. return;
  110. }
  111. const {name: firstId} = left;
  112. const {
  113. left: {name: secondId},
  114. right: {raw: literal},
  115. } = right;
  116. // Parameter is reassigned to a different identifier
  117. if (assignment && firstId !== secondId) {
  118. return;
  119. }
  120. const variable = findVariable(context.getScope(), secondId);
  121. // This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1122
  122. // But can't reproduce, just ignore this case
  123. /* c8 ignore next 3 */
  124. if (!variable) {
  125. return;
  126. }
  127. const {references} = variable;
  128. const {params} = currentFunction;
  129. const parameter = params.find(parameter =>
  130. parameter.type === 'Identifier'
  131. && parameter.name === secondId,
  132. );
  133. if (
  134. hasSideEffects(sourceCode, currentFunction, node)
  135. || hasExtraReferences(assignment, references, left)
  136. || !isLastParameter(params, parameter)
  137. ) {
  138. return;
  139. }
  140. const replacement = needsParentheses(sourceCode, currentFunction)
  141. ? `(${firstId} = ${literal})`
  142. : `${firstId} = ${literal}`;
  143. return {
  144. node,
  145. messageId: MESSAGE_ID,
  146. suggest: [{
  147. messageId: MESSAGE_ID_SUGGEST,
  148. fix: fixer => [
  149. fixer.replaceText(parameter, replacement),
  150. fixDefaultExpression(fixer, sourceCode, node),
  151. ],
  152. }],
  153. };
  154. };
  155. return {
  156. ':function'(node) {
  157. functionStack.push(node);
  158. },
  159. ':function:exit'() {
  160. functionStack.pop();
  161. },
  162. [assignmentSelector](node) {
  163. const {left, right} = node.expression;
  164. return checkExpression(node, left, right, true);
  165. },
  166. [declarationSelector](node) {
  167. const {id, init} = node.declarations[0];
  168. return checkExpression(node, id, init, false);
  169. },
  170. };
  171. };
  172. /** @type {import('eslint').Rule.RuleModule} */
  173. module.exports = {
  174. create,
  175. meta: {
  176. type: 'suggestion',
  177. docs: {
  178. description: 'Prefer default parameters over reassignment.',
  179. },
  180. fixable: 'code',
  181. hasSuggestions: true,
  182. messages: {
  183. [MESSAGE_ID]: 'Prefer default parameters over reassignment.',
  184. [MESSAGE_ID_SUGGEST]: 'Replace reassignment with default parameter.',
  185. },
  186. },
  187. };