destructuring-assignment.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  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 report = require('../util/report');
  9. const DEFAULT_OPTION = 'always';
  10. function createSFCParams() {
  11. const queue = [];
  12. return {
  13. push(params) {
  14. queue.unshift(params);
  15. },
  16. pop() {
  17. queue.shift();
  18. },
  19. propsName() {
  20. const found = queue.find((params) => {
  21. const props = params[0];
  22. return props && !props.destructuring && props.name;
  23. });
  24. return found && found[0] && found[0].name;
  25. },
  26. contextName() {
  27. const found = queue.find((params) => {
  28. const context = params[1];
  29. return context && !context.destructuring && context.name;
  30. });
  31. return found && found[1] && found[1].name;
  32. },
  33. };
  34. }
  35. function evalParams(params) {
  36. return params.map((param) => ({
  37. destructuring: param.type === 'ObjectPattern',
  38. name: param.type === 'Identifier' && param.name,
  39. }));
  40. }
  41. const messages = {
  42. noDestructPropsInSFCArg: 'Must never use destructuring props assignment in SFC argument',
  43. noDestructContextInSFCArg: 'Must never use destructuring context assignment in SFC argument',
  44. noDestructAssignment: 'Must never use destructuring {{type}} assignment',
  45. useDestructAssignment: 'Must use destructuring {{type}} assignment',
  46. destructureInSignature: 'Must destructure props in the function signature.',
  47. };
  48. module.exports = {
  49. meta: {
  50. docs: {
  51. description: 'Enforce consistent usage of destructuring assignment of props, state, and context',
  52. category: 'Stylistic Issues',
  53. recommended: false,
  54. url: docsUrl('destructuring-assignment'),
  55. },
  56. fixable: 'code',
  57. messages,
  58. schema: [{
  59. type: 'string',
  60. enum: [
  61. 'always',
  62. 'never',
  63. ],
  64. }, {
  65. type: 'object',
  66. properties: {
  67. ignoreClassFields: {
  68. type: 'boolean',
  69. },
  70. destructureInSignature: {
  71. type: 'string',
  72. enum: [
  73. 'always',
  74. 'ignore',
  75. ],
  76. },
  77. },
  78. additionalProperties: false,
  79. }],
  80. },
  81. create: Components.detect((context, components, utils) => {
  82. const configuration = context.options[0] || DEFAULT_OPTION;
  83. const ignoreClassFields = (context.options[1] && (context.options[1].ignoreClassFields === true)) || false;
  84. const destructureInSignature = (context.options[1] && context.options[1].destructureInSignature) || 'ignore';
  85. const sfcParams = createSFCParams();
  86. /**
  87. * @param {ASTNode} node We expect either an ArrowFunctionExpression,
  88. * FunctionDeclaration, or FunctionExpression
  89. */
  90. function handleStatelessComponent(node) {
  91. const params = evalParams(node.params);
  92. const SFCComponent = components.get(context.getScope(node).block);
  93. if (!SFCComponent) {
  94. return;
  95. }
  96. sfcParams.push(params);
  97. if (params[0] && params[0].destructuring && components.get(node) && configuration === 'never') {
  98. report(context, messages.noDestructPropsInSFCArg, 'noDestructPropsInSFCArg', {
  99. node,
  100. });
  101. } else if (params[1] && params[1].destructuring && components.get(node) && configuration === 'never') {
  102. report(context, messages.noDestructContextInSFCArg, 'noDestructContextInSFCArg', {
  103. node,
  104. });
  105. }
  106. }
  107. function handleStatelessComponentExit(node) {
  108. const SFCComponent = components.get(context.getScope(node).block);
  109. if (SFCComponent) {
  110. sfcParams.pop();
  111. }
  112. }
  113. function handleSFCUsage(node) {
  114. const propsName = sfcParams.propsName();
  115. const contextName = sfcParams.contextName();
  116. // props.aProp || context.aProp
  117. const isPropUsed = (
  118. (propsName && node.object.name === propsName)
  119. || (contextName && node.object.name === contextName)
  120. )
  121. && !isAssignmentLHS(node);
  122. if (isPropUsed && configuration === 'always' && !node.optional) {
  123. report(context, messages.useDestructAssignment, 'useDestructAssignment', {
  124. node,
  125. data: {
  126. type: node.object.name,
  127. },
  128. });
  129. }
  130. }
  131. function isInClassProperty(node) {
  132. let curNode = node.parent;
  133. while (curNode) {
  134. if (curNode.type === 'ClassProperty' || curNode.type === 'PropertyDefinition') {
  135. return true;
  136. }
  137. curNode = curNode.parent;
  138. }
  139. return false;
  140. }
  141. function handleClassUsage(node) {
  142. // this.props.Aprop || this.context.aProp || this.state.aState
  143. const isPropUsed = (
  144. node.object.type === 'MemberExpression' && node.object.object.type === 'ThisExpression'
  145. && (node.object.property.name === 'props' || node.object.property.name === 'context' || node.object.property.name === 'state')
  146. && !isAssignmentLHS(node)
  147. );
  148. if (
  149. isPropUsed && configuration === 'always'
  150. && !(ignoreClassFields && isInClassProperty(node))
  151. ) {
  152. report(context, messages.useDestructAssignment, 'useDestructAssignment', {
  153. node,
  154. data: {
  155. type: node.object.property.name,
  156. },
  157. });
  158. }
  159. }
  160. return {
  161. FunctionDeclaration: handleStatelessComponent,
  162. ArrowFunctionExpression: handleStatelessComponent,
  163. FunctionExpression: handleStatelessComponent,
  164. 'FunctionDeclaration:exit': handleStatelessComponentExit,
  165. 'ArrowFunctionExpression:exit': handleStatelessComponentExit,
  166. 'FunctionExpression:exit': handleStatelessComponentExit,
  167. MemberExpression(node) {
  168. let scope = context.getScope(node);
  169. let SFCComponent = components.get(scope.block);
  170. while (!SFCComponent && scope.upper && scope.upper !== scope) {
  171. SFCComponent = components.get(scope.upper.block);
  172. scope = scope.upper;
  173. }
  174. if (SFCComponent) {
  175. handleSFCUsage(node);
  176. }
  177. const classComponent = utils.getParentComponent(node);
  178. if (classComponent) {
  179. handleClassUsage(node);
  180. }
  181. },
  182. VariableDeclarator(node) {
  183. const classComponent = utils.getParentComponent(node);
  184. const SFCComponent = components.get(context.getScope(node).block);
  185. const destructuring = (node.init && node.id && node.id.type === 'ObjectPattern');
  186. // let {foo} = props;
  187. const destructuringSFC = destructuring && (node.init.name === 'props' || node.init.name === 'context');
  188. // let {foo} = this.props;
  189. const destructuringClass = destructuring && node.init.object && node.init.object.type === 'ThisExpression' && (
  190. node.init.property.name === 'props' || node.init.property.name === 'context' || node.init.property.name === 'state'
  191. );
  192. if (SFCComponent && destructuringSFC && configuration === 'never') {
  193. report(context, messages.noDestructAssignment, 'noDestructAssignment', {
  194. node,
  195. data: {
  196. type: node.init.name,
  197. },
  198. });
  199. }
  200. if (
  201. classComponent && destructuringClass && configuration === 'never'
  202. && !(ignoreClassFields && (node.parent.type === 'ClassProperty' || node.parent.type === 'PropertyDefinition'))
  203. ) {
  204. report(context, messages.noDestructAssignment, 'noDestructAssignment', {
  205. node,
  206. data: {
  207. type: node.init.property.name,
  208. },
  209. });
  210. }
  211. if (
  212. SFCComponent
  213. && destructuringSFC
  214. && configuration === 'always'
  215. && destructureInSignature === 'always'
  216. && node.init.name === 'props'
  217. ) {
  218. const scopeSetProps = context.getScope().set.get('props');
  219. const propsRefs = scopeSetProps && scopeSetProps.references;
  220. if (!propsRefs) {
  221. return;
  222. }
  223. // Skip if props is used elsewhere
  224. if (propsRefs.length > 1) {
  225. return;
  226. }
  227. report(context, messages.destructureInSignature, 'destructureInSignature', {
  228. node,
  229. fix(fixer) {
  230. const param = SFCComponent.node.params[0];
  231. if (!param) {
  232. return;
  233. }
  234. const replaceRange = [
  235. param.range[0],
  236. param.typeAnnotation ? param.typeAnnotation.range[0] : param.range[1],
  237. ];
  238. return [
  239. fixer.replaceTextRange(replaceRange, context.getSourceCode().getText(node.id)),
  240. fixer.remove(node.parent),
  241. ];
  242. },
  243. });
  244. }
  245. },
  246. };
  247. }),
  248. };