destructuring-assignment.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. /**
  2. * @fileoverview Enforce consistent usage of destructuring assignment of props, state, and context.
  3. */
  4. 'use strict';
  5. const Components = require('../util/Components');
  6. const docsUrl = require('../util/docsUrl');
  7. const isAssignmentLHS = require('../util/ast').isAssignmentLHS;
  8. const DEFAULT_OPTION = 'always';
  9. function createSFCParams() {
  10. const queue = [];
  11. return {
  12. push(params) {
  13. queue.unshift(params);
  14. },
  15. pop() {
  16. queue.shift();
  17. },
  18. propsName() {
  19. const found = queue.find((params) => {
  20. const props = params[0];
  21. return props && !props.destructuring && props.name;
  22. });
  23. return found && found[0] && found[0].name;
  24. },
  25. contextName() {
  26. const found = queue.find((params) => {
  27. const context = params[1];
  28. return context && !context.destructuring && context.name;
  29. });
  30. return found && found[1] && found.name;
  31. }
  32. };
  33. }
  34. function evalParams(params) {
  35. return params.map((param) => ({
  36. destructuring: param.type === 'ObjectPattern',
  37. name: param.type === 'Identifier' && param.name
  38. }));
  39. }
  40. module.exports = {
  41. meta: {
  42. docs: {
  43. description: 'Enforce consistent usage of destructuring assignment of props, state, and context',
  44. category: 'Stylistic Issues',
  45. recommended: false,
  46. url: docsUrl('destructuring-assignment')
  47. },
  48. messages: {
  49. noDestructPropsInSFCArg: 'Must never use destructuring props assignment in SFC argument',
  50. noDestructContextInSFCArg: 'Must never use destructuring context assignment in SFC argument',
  51. noDestructAssignment: 'Must never use destructuring {{type}} assignment',
  52. useDestructAssignment: 'Must use destructuring {{type}} assignment'
  53. },
  54. schema: [{
  55. type: 'string',
  56. enum: [
  57. 'always',
  58. 'never'
  59. ]
  60. }, {
  61. type: 'object',
  62. properties: {
  63. ignoreClassFields: {
  64. type: 'boolean'
  65. }
  66. },
  67. additionalProperties: false
  68. }]
  69. },
  70. create: Components.detect((context, components, utils) => {
  71. const configuration = context.options[0] || DEFAULT_OPTION;
  72. const ignoreClassFields = (context.options[1] && (context.options[1].ignoreClassFields === true)) || false;
  73. const sfcParams = createSFCParams();
  74. /**
  75. * @param {ASTNode} node We expect either an ArrowFunctionExpression,
  76. * FunctionDeclaration, or FunctionExpression
  77. */
  78. function handleStatelessComponent(node) {
  79. const params = evalParams(node.params);
  80. const SFCComponent = components.get(context.getScope(node).block);
  81. if (!SFCComponent) {
  82. return;
  83. }
  84. sfcParams.push(params);
  85. if (params[0] && params[0].destructuring && components.get(node) && configuration === 'never') {
  86. context.report({
  87. node,
  88. messageId: 'noDestructPropsInSFCArg'
  89. });
  90. } else if (params[1] && params[1].destructuring && components.get(node) && configuration === 'never') {
  91. context.report({
  92. node,
  93. messageId: 'noDestructContextInSFCArg'
  94. });
  95. }
  96. }
  97. function handleStatelessComponentExit(node) {
  98. const SFCComponent = components.get(context.getScope(node).block);
  99. if (SFCComponent) {
  100. sfcParams.pop();
  101. }
  102. }
  103. function handleSFCUsage(node) {
  104. const propsName = sfcParams.propsName();
  105. const contextName = sfcParams.contextName();
  106. // props.aProp || context.aProp
  107. const isPropUsed = (
  108. (propsName && node.object.name === propsName)
  109. || (contextName && node.object.name === contextName)
  110. )
  111. && !isAssignmentLHS(node);
  112. if (isPropUsed && configuration === 'always') {
  113. context.report({
  114. node,
  115. messageId: 'useDestructAssignment',
  116. data: {
  117. type: node.object.name
  118. }
  119. });
  120. }
  121. }
  122. function isInClassProperty(node) {
  123. let curNode = node.parent;
  124. while (curNode) {
  125. if (curNode.type === 'ClassProperty') {
  126. return true;
  127. }
  128. curNode = curNode.parent;
  129. }
  130. return false;
  131. }
  132. function handleClassUsage(node) {
  133. // this.props.Aprop || this.context.aProp || this.state.aState
  134. const isPropUsed = (
  135. node.object.type === 'MemberExpression' && node.object.object.type === 'ThisExpression'
  136. && (node.object.property.name === 'props' || node.object.property.name === 'context' || node.object.property.name === 'state')
  137. && !isAssignmentLHS(node)
  138. );
  139. if (
  140. isPropUsed && configuration === 'always'
  141. && !(ignoreClassFields && isInClassProperty(node))
  142. ) {
  143. context.report({
  144. node,
  145. messageId: 'useDestructAssignment',
  146. data: {
  147. type: node.object.property.name
  148. }
  149. });
  150. }
  151. }
  152. return {
  153. FunctionDeclaration: handleStatelessComponent,
  154. ArrowFunctionExpression: handleStatelessComponent,
  155. FunctionExpression: handleStatelessComponent,
  156. 'FunctionDeclaration:exit': handleStatelessComponentExit,
  157. 'ArrowFunctionExpression:exit': handleStatelessComponentExit,
  158. 'FunctionExpression:exit': handleStatelessComponentExit,
  159. MemberExpression(node) {
  160. const SFCComponent = components.get(context.getScope(node).block);
  161. const classComponent = utils.getParentComponent(node);
  162. if (SFCComponent) {
  163. handleSFCUsage(node);
  164. }
  165. if (classComponent) {
  166. handleClassUsage(node);
  167. }
  168. },
  169. VariableDeclarator(node) {
  170. const classComponent = utils.getParentComponent(node);
  171. const SFCComponent = components.get(context.getScope(node).block);
  172. const destructuring = (node.init && node.id && node.id.type === 'ObjectPattern');
  173. // let {foo} = props;
  174. const destructuringSFC = destructuring && (node.init.name === 'props' || node.init.name === 'context');
  175. // let {foo} = this.props;
  176. const destructuringClass = destructuring && node.init.object && node.init.object.type === 'ThisExpression' && (
  177. node.init.property.name === 'props' || node.init.property.name === 'context' || node.init.property.name === 'state'
  178. );
  179. if (SFCComponent && destructuringSFC && configuration === 'never') {
  180. context.report({
  181. node,
  182. messageId: 'noDestructAssignment',
  183. data: {
  184. type: node.init.name
  185. }
  186. });
  187. }
  188. if (
  189. classComponent && destructuringClass && configuration === 'never'
  190. && !(ignoreClassFields && node.parent.type === 'ClassProperty')
  191. ) {
  192. context.report({
  193. node,
  194. messageId: 'noDestructAssignment',
  195. data: {
  196. type: node.init.property.name
  197. }
  198. });
  199. }
  200. }
  201. };
  202. })
  203. };