prefer-stateless-function.js 12 KB

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