prefer-stateless-function.js 13 KB

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