destructuring-assignment.js 9.7 KB

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