consistent-destructuring.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. 'use strict';
  2. const avoidCapture = require('./utils/avoid-capture.js');
  3. const {not, notLeftHandSideSelector} = require('./selectors/index.js');
  4. const MESSAGE_ID = 'consistentDestructuring';
  5. const MESSAGE_ID_SUGGEST = 'consistentDestructuringSuggest';
  6. const declaratorSelector = [
  7. 'VariableDeclarator',
  8. '[id.type="ObjectPattern"]',
  9. '[init]',
  10. '[init.type!="Literal"]',
  11. ].join('');
  12. const memberSelector = [
  13. 'MemberExpression',
  14. '[computed!=true]',
  15. notLeftHandSideSelector(),
  16. not([
  17. 'CallExpression > .callee',
  18. 'NewExpression> .callee',
  19. ]),
  20. ].join('');
  21. const isSimpleExpression = expression => {
  22. while (expression) {
  23. if (expression.computed) {
  24. return false;
  25. }
  26. if (expression.type !== 'MemberExpression') {
  27. break;
  28. }
  29. expression = expression.object;
  30. }
  31. return expression.type === 'Identifier'
  32. || expression.type === 'ThisExpression';
  33. };
  34. const isChildInParentScope = (child, parent) => {
  35. while (child) {
  36. if (child === parent) {
  37. return true;
  38. }
  39. child = child.upper;
  40. }
  41. return false;
  42. };
  43. /** @param {import('eslint').Rule.RuleContext} context */
  44. const create = context => {
  45. const source = context.getSourceCode();
  46. const declarations = new Map();
  47. return {
  48. [declaratorSelector](node) {
  49. // Ignore any complex expressions (e.g. arrays, functions)
  50. if (!isSimpleExpression(node.init)) {
  51. return;
  52. }
  53. declarations.set(source.getText(node.init), {
  54. scope: context.getScope(),
  55. variables: context.getDeclaredVariables(node),
  56. objectPattern: node.id,
  57. });
  58. },
  59. [memberSelector](node) {
  60. const declaration = declarations.get(source.getText(node.object));
  61. if (!declaration) {
  62. return;
  63. }
  64. const {scope, objectPattern} = declaration;
  65. const memberScope = context.getScope();
  66. // Property is destructured outside the current scope
  67. if (!isChildInParentScope(memberScope, scope)) {
  68. return;
  69. }
  70. const destructurings = objectPattern.properties.filter(property =>
  71. property.type === 'Property'
  72. && property.key.type === 'Identifier'
  73. && property.value.type === 'Identifier',
  74. );
  75. const lastProperty = objectPattern.properties[objectPattern.properties.length - 1];
  76. const hasRest = lastProperty && lastProperty.type === 'RestElement';
  77. const expression = source.getText(node);
  78. const member = source.getText(node.property);
  79. // Member might already be destructured
  80. const destructuredMember = destructurings.find(property =>
  81. property.key.name === member,
  82. );
  83. if (!destructuredMember) {
  84. // Don't destructure additional members when rest is used
  85. if (hasRest) {
  86. return;
  87. }
  88. // Destructured member collides with an existing identifier
  89. if (avoidCapture(member, [memberScope]) !== member) {
  90. return;
  91. }
  92. }
  93. // Don't try to fix nested member expressions
  94. if (node.parent.type === 'MemberExpression') {
  95. return {
  96. node,
  97. messageId: MESSAGE_ID,
  98. };
  99. }
  100. const newMember = destructuredMember ? destructuredMember.value.name : member;
  101. return {
  102. node,
  103. messageId: MESSAGE_ID,
  104. suggest: [{
  105. messageId: MESSAGE_ID_SUGGEST,
  106. data: {
  107. expression,
  108. property: newMember,
  109. },
  110. * fix(fixer) {
  111. const {properties} = objectPattern;
  112. const lastProperty = properties[properties.length - 1];
  113. yield fixer.replaceText(node, newMember);
  114. if (!destructuredMember) {
  115. yield lastProperty
  116. ? fixer.insertTextAfter(lastProperty, `, ${newMember}`)
  117. : fixer.replaceText(objectPattern, `{${newMember}}`);
  118. }
  119. },
  120. }],
  121. };
  122. },
  123. };
  124. };
  125. /** @type {import('eslint').Rule.RuleModule} */
  126. module.exports = {
  127. create,
  128. meta: {
  129. type: 'suggestion',
  130. docs: {
  131. description: 'Use destructured variables over properties.',
  132. },
  133. fixable: 'code',
  134. hasSuggestions: true,
  135. messages: {
  136. [MESSAGE_ID]: 'Use destructured variables over properties.',
  137. [MESSAGE_ID_SUGGEST]: 'Replace `{{expression}}` with destructured property `{{property}}`.',
  138. },
  139. },
  140. };