prefer-stateless-function.js 12 KB

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