destructuring-assignment.js 10 KB

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