prefer-stateless-function.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. /**
  2. * @fileoverview Enforce stateless components to be written as a pure function
  3. * @author Yannick Croissant
  4. * @author Alberto Rodríguez
  5. * @copyright 2015 Alberto Rodríguez. All rights reserved.
  6. */
  7. 'use strict';
  8. const values = require('object.values');
  9. const Components = require('../util/Components');
  10. const testReactVersion = require('../util/version').testReactVersion;
  11. const astUtil = require('../util/ast');
  12. const componentUtil = require('../util/componentUtil');
  13. const docsUrl = require('../util/docsUrl');
  14. const report = require('../util/report');
  15. const eslintUtil = require('../util/eslint');
  16. const getScope = eslintUtil.getScope;
  17. const getText = eslintUtil.getText;
  18. // ------------------------------------------------------------------------------
  19. // Rule Definition
  20. // ------------------------------------------------------------------------------
  21. const messages = {
  22. componentShouldBePure: 'Component should be written as a pure function',
  23. };
  24. /** @type {import('eslint').Rule.RuleModule} */
  25. module.exports = {
  26. meta: {
  27. docs: {
  28. description: 'Enforce stateless components to be written as a pure function',
  29. category: 'Stylistic Issues',
  30. recommended: false,
  31. url: docsUrl('prefer-stateless-function'),
  32. },
  33. messages,
  34. schema: [{
  35. type: 'object',
  36. properties: {
  37. ignorePureComponents: {
  38. default: false,
  39. type: 'boolean',
  40. },
  41. },
  42. additionalProperties: false,
  43. }],
  44. },
  45. create: Components.detect((context, components, utils) => {
  46. const configuration = context.options[0] || {};
  47. const ignorePureComponents = configuration.ignorePureComponents || false;
  48. // --------------------------------------------------------------------------
  49. // Public
  50. // --------------------------------------------------------------------------
  51. /**
  52. * Checks whether a given array of statements is a single call of `super`.
  53. * @see eslint no-useless-constructor rule
  54. * @param {ASTNode[]} body - An array of statements to check.
  55. * @returns {boolean} `true` if the body is a single call of `super`.
  56. */
  57. function isSingleSuperCall(body) {
  58. return (
  59. body.length === 1
  60. && body[0].type === 'ExpressionStatement'
  61. && astUtil.isCallExpression(body[0].expression)
  62. && body[0].expression.callee.type === 'Super'
  63. );
  64. }
  65. /**
  66. * Checks whether a given node is a pattern which doesn't have any side effects.
  67. * Default parameters and Destructuring parameters can have side effects.
  68. * @see eslint no-useless-constructor rule
  69. * @param {ASTNode} node - A pattern node.
  70. * @returns {boolean} `true` if the node doesn't have any side effects.
  71. */
  72. function isSimple(node) {
  73. return node.type === 'Identifier' || node.type === 'RestElement';
  74. }
  75. /**
  76. * Checks whether a given array of expressions is `...arguments` or not.
  77. * `super(...arguments)` passes all arguments through.
  78. * @see eslint no-useless-constructor rule
  79. * @param {ASTNode[]} superArgs - An array of expressions to check.
  80. * @returns {boolean} `true` if the superArgs is `...arguments`.
  81. */
  82. function isSpreadArguments(superArgs) {
  83. return (
  84. superArgs.length === 1
  85. && superArgs[0].type === 'SpreadElement'
  86. && superArgs[0].argument.type === 'Identifier'
  87. && superArgs[0].argument.name === 'arguments'
  88. );
  89. }
  90. /**
  91. * Checks whether given 2 nodes are identifiers which have the same name or not.
  92. * @see eslint no-useless-constructor rule
  93. * @param {ASTNode} ctorParam - A node to check.
  94. * @param {ASTNode} superArg - A node to check.
  95. * @returns {boolean} `true` if the nodes are identifiers which have the same
  96. * name.
  97. */
  98. function isValidIdentifierPair(ctorParam, superArg) {
  99. return (
  100. ctorParam.type === 'Identifier'
  101. && superArg.type === 'Identifier'
  102. && ctorParam.name === superArg.name
  103. );
  104. }
  105. /**
  106. * Checks whether given 2 nodes are a rest/spread pair which has the same values.
  107. * @see eslint no-useless-constructor rule
  108. * @param {ASTNode} ctorParam - A node to check.
  109. * @param {ASTNode} superArg - A node to check.
  110. * @returns {boolean} `true` if the nodes are a rest/spread pair which has the
  111. * same values.
  112. */
  113. function isValidRestSpreadPair(ctorParam, superArg) {
  114. return (
  115. ctorParam.type === 'RestElement'
  116. && superArg.type === 'SpreadElement'
  117. && isValidIdentifierPair(ctorParam.argument, superArg.argument)
  118. );
  119. }
  120. /**
  121. * Checks whether given 2 nodes have the same value or not.
  122. * @see eslint no-useless-constructor rule
  123. * @param {ASTNode} ctorParam - A node to check.
  124. * @param {ASTNode} superArg - A node to check.
  125. * @returns {boolean} `true` if the nodes have the same value or not.
  126. */
  127. function isValidPair(ctorParam, superArg) {
  128. return (
  129. isValidIdentifierPair(ctorParam, superArg)
  130. || isValidRestSpreadPair(ctorParam, superArg)
  131. );
  132. }
  133. /**
  134. * Checks whether the parameters of a constructor and the arguments of `super()`
  135. * have the same values or not.
  136. * @see eslint no-useless-constructor rule
  137. * @param {ASTNode[]} ctorParams - The parameters of a constructor to check.
  138. * @param {ASTNode} superArgs - The arguments of `super()` to check.
  139. * @returns {boolean} `true` if those have the same values.
  140. */
  141. function isPassingThrough(ctorParams, superArgs) {
  142. if (ctorParams.length !== superArgs.length) {
  143. return false;
  144. }
  145. for (let i = 0; i < ctorParams.length; ++i) {
  146. if (!isValidPair(ctorParams[i], superArgs[i])) {
  147. return false;
  148. }
  149. }
  150. return true;
  151. }
  152. /**
  153. * Checks whether the constructor body is a redundant super call.
  154. * @see eslint no-useless-constructor rule
  155. * @param {Array} body - constructor body content.
  156. * @param {Array} ctorParams - The params to check against super call.
  157. * @returns {boolean} true if the constructor body is redundant
  158. */
  159. function isRedundantSuperCall(body, ctorParams) {
  160. return (
  161. isSingleSuperCall(body)
  162. && ctorParams.every(isSimple)
  163. && (
  164. isSpreadArguments(body[0].expression.arguments)
  165. || isPassingThrough(ctorParams, body[0].expression.arguments)
  166. )
  167. );
  168. }
  169. /**
  170. * Check if a given AST node have any other properties the ones available in stateless components
  171. * @param {ASTNode} node The AST node being checked.
  172. * @returns {boolean} True if the node has at least one other property, false if not.
  173. */
  174. function hasOtherProperties(node) {
  175. const properties = astUtil.getComponentProperties(node);
  176. return properties.some((property) => {
  177. const name = astUtil.getPropertyName(property);
  178. const isDisplayName = name === 'displayName';
  179. const isPropTypes = name === 'propTypes' || ((name === 'props') && property.typeAnnotation);
  180. const contextTypes = name === 'contextTypes';
  181. const defaultProps = name === 'defaultProps';
  182. const isUselessConstructor = property.kind === 'constructor'
  183. && !!property.value.body
  184. && isRedundantSuperCall(property.value.body.body, property.value.params);
  185. const isRender = name === 'render';
  186. return !isDisplayName && !isPropTypes && !contextTypes && !defaultProps && !isUselessConstructor && !isRender;
  187. });
  188. }
  189. /**
  190. * Mark component as pure as declared
  191. * @param {ASTNode} node The AST node being checked.
  192. */
  193. function markSCUAsDeclared(node) {
  194. components.set(node, {
  195. hasSCU: true,
  196. });
  197. }
  198. /**
  199. * Mark childContextTypes as declared
  200. * @param {ASTNode} node The AST node being checked.
  201. */
  202. function markChildContextTypesAsDeclared(node) {
  203. components.set(node, {
  204. hasChildContextTypes: true,
  205. });
  206. }
  207. /**
  208. * Mark a setState as used
  209. * @param {ASTNode} node The AST node being checked.
  210. */
  211. function markThisAsUsed(node) {
  212. components.set(node, {
  213. useThis: true,
  214. });
  215. }
  216. /**
  217. * Mark a props or context as used
  218. * @param {ASTNode} node The AST node being checked.
  219. */
  220. function markPropsOrContextAsUsed(node) {
  221. components.set(node, {
  222. usePropsOrContext: true,
  223. });
  224. }
  225. /**
  226. * Mark a ref as used
  227. * @param {ASTNode} node The AST node being checked.
  228. */
  229. function markRefAsUsed(node) {
  230. components.set(node, {
  231. useRef: true,
  232. });
  233. }
  234. /**
  235. * Mark return as invalid
  236. * @param {ASTNode} node The AST node being checked.
  237. */
  238. function markReturnAsInvalid(node) {
  239. components.set(node, {
  240. invalidReturn: true,
  241. });
  242. }
  243. /**
  244. * Mark a ClassDeclaration as having used decorators
  245. * @param {ASTNode} node The AST node being checked.
  246. */
  247. function markDecoratorsAsUsed(node) {
  248. components.set(node, {
  249. useDecorators: true,
  250. });
  251. }
  252. function visitClass(node) {
  253. if (ignorePureComponents && componentUtil.isPureComponent(node, context)) {
  254. markSCUAsDeclared(node);
  255. }
  256. if (node.decorators && node.decorators.length) {
  257. markDecoratorsAsUsed(node);
  258. }
  259. }
  260. return {
  261. ClassDeclaration: visitClass,
  262. ClassExpression: visitClass,
  263. // Mark `this` destructuring as a usage of `this`
  264. VariableDeclarator(node) {
  265. // Ignore destructuring on other than `this`
  266. if (!node.id || node.id.type !== 'ObjectPattern' || !node.init || node.init.type !== 'ThisExpression') {
  267. return;
  268. }
  269. // Ignore `props` and `context`
  270. const useThis = node.id.properties.some((property) => {
  271. const name = astUtil.getPropertyName(property);
  272. return name !== 'props' && name !== 'context';
  273. });
  274. if (!useThis) {
  275. markPropsOrContextAsUsed(node);
  276. return;
  277. }
  278. markThisAsUsed(node);
  279. },
  280. // Mark `this` usage
  281. MemberExpression(node) {
  282. if (node.object.type !== 'ThisExpression') {
  283. if (node.property && node.property.name === 'childContextTypes') {
  284. const component = utils.getRelatedComponent(node);
  285. if (!component) {
  286. return;
  287. }
  288. markChildContextTypesAsDeclared(component.node);
  289. }
  290. return;
  291. // Ignore calls to `this.props` and `this.context`
  292. }
  293. if (
  294. (node.property.name || node.property.value) === 'props'
  295. || (node.property.name || node.property.value) === 'context'
  296. ) {
  297. markPropsOrContextAsUsed(node);
  298. return;
  299. }
  300. markThisAsUsed(node);
  301. },
  302. // Mark `ref` usage
  303. JSXAttribute(node) {
  304. const name = getText(context, node.name);
  305. if (name !== 'ref') {
  306. return;
  307. }
  308. markRefAsUsed(node);
  309. },
  310. // Mark `render` that do not return some JSX
  311. ReturnStatement(node) {
  312. let blockNode;
  313. let scope = getScope(context, node);
  314. while (scope) {
  315. blockNode = scope.block && scope.block.parent;
  316. if (blockNode && (blockNode.type === 'MethodDefinition' || blockNode.type === 'Property')) {
  317. break;
  318. }
  319. scope = scope.upper;
  320. }
  321. const isRender = blockNode
  322. && blockNode.key
  323. && blockNode.key.name === 'render';
  324. const allowNull = testReactVersion(context, '>= 15.0.0'); // Stateless components can return null since React 15
  325. const isReturningJSX = utils.isReturningJSX(node, !allowNull);
  326. const isReturningNull = node.argument && (node.argument.value === null || node.argument.value === false);
  327. if (
  328. !isRender
  329. || (allowNull && (isReturningJSX || isReturningNull))
  330. || (!allowNull && isReturningJSX)
  331. ) {
  332. return;
  333. }
  334. markReturnAsInvalid(node);
  335. },
  336. 'Program:exit'() {
  337. const list = components.list();
  338. values(list)
  339. .filter((component) => (
  340. !hasOtherProperties(component.node)
  341. && !component.useThis
  342. && !component.useRef
  343. && !component.invalidReturn
  344. && !component.hasChildContextTypes
  345. && !component.useDecorators
  346. && !component.hasSCU
  347. && (
  348. componentUtil.isES5Component(component.node, context)
  349. || componentUtil.isES6Component(component.node, context)
  350. )
  351. ))
  352. .forEach((component) => {
  353. report(context, messages.componentShouldBePure, 'componentShouldBePure', {
  354. node: component.node,
  355. });
  356. });
  357. },
  358. };
  359. }),
  360. };