require-optimization.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. /**
  2. * @fileoverview Enforce React components to have a shouldComponentUpdate method
  3. * @author Evgueni Naverniouk
  4. */
  5. 'use strict';
  6. const values = require('object.values');
  7. const Components = require('../util/Components');
  8. const componentUtil = require('../util/componentUtil');
  9. const docsUrl = require('../util/docsUrl');
  10. const report = require('../util/report');
  11. const messages = {
  12. noShouldComponentUpdate: 'Component is not optimized. Please add a shouldComponentUpdate method.',
  13. };
  14. module.exports = {
  15. meta: {
  16. docs: {
  17. description: 'Enforce React components to have a shouldComponentUpdate method',
  18. category: 'Best Practices',
  19. recommended: false,
  20. url: docsUrl('require-optimization'),
  21. },
  22. messages,
  23. schema: [{
  24. type: 'object',
  25. properties: {
  26. allowDecorators: {
  27. type: 'array',
  28. items: {
  29. type: 'string',
  30. },
  31. },
  32. },
  33. additionalProperties: false,
  34. }],
  35. },
  36. create: Components.detect((context, components) => {
  37. const configuration = context.options[0] || {};
  38. const allowDecorators = configuration.allowDecorators || [];
  39. /**
  40. * Checks to see if our component is decorated by PureRenderMixin via reactMixin
  41. * @param {ASTNode} node The AST node being checked.
  42. * @returns {Boolean} True if node is decorated with a PureRenderMixin, false if not.
  43. */
  44. function hasPureRenderDecorator(node) {
  45. if (node.decorators && node.decorators.length) {
  46. for (let i = 0, l = node.decorators.length; i < l; i++) {
  47. if (
  48. node.decorators[i].expression
  49. && node.decorators[i].expression.callee
  50. && node.decorators[i].expression.callee.object
  51. && node.decorators[i].expression.callee.object.name === 'reactMixin'
  52. && node.decorators[i].expression.callee.property
  53. && node.decorators[i].expression.callee.property.name === 'decorate'
  54. && node.decorators[i].expression.arguments
  55. && node.decorators[i].expression.arguments.length
  56. && node.decorators[i].expression.arguments[0].name === 'PureRenderMixin'
  57. ) {
  58. return true;
  59. }
  60. }
  61. }
  62. return false;
  63. }
  64. /**
  65. * Checks to see if our component is custom decorated
  66. * @param {ASTNode} node The AST node being checked.
  67. * @returns {Boolean} True if node is decorated name with a custom decorated, false if not.
  68. */
  69. function hasCustomDecorator(node) {
  70. const allowLength = allowDecorators.length;
  71. if (allowLength && node.decorators && node.decorators.length) {
  72. for (let i = 0; i < allowLength; i++) {
  73. for (let j = 0, l = node.decorators.length; j < l; j++) {
  74. if (
  75. node.decorators[j].expression
  76. && node.decorators[j].expression.name === allowDecorators[i]
  77. ) {
  78. return true;
  79. }
  80. }
  81. }
  82. }
  83. return false;
  84. }
  85. /**
  86. * Checks if we are declaring a shouldComponentUpdate method
  87. * @param {ASTNode} node The AST node being checked.
  88. * @returns {Boolean} True if we are declaring a shouldComponentUpdate method, false if not.
  89. */
  90. function isSCUDeclared(node) {
  91. return Boolean(
  92. node
  93. && node.name === 'shouldComponentUpdate'
  94. );
  95. }
  96. /**
  97. * Checks if we are declaring a PureRenderMixin mixin
  98. * @param {ASTNode} node The AST node being checked.
  99. * @returns {Boolean} True if we are declaring a PureRenderMixin method, false if not.
  100. */
  101. function isPureRenderDeclared(node) {
  102. let hasPR = false;
  103. if (node.value && node.value.elements) {
  104. for (let i = 0, l = node.value.elements.length; i < l; i++) {
  105. if (node.value.elements[i] && node.value.elements[i].name === 'PureRenderMixin') {
  106. hasPR = true;
  107. break;
  108. }
  109. }
  110. }
  111. return Boolean(
  112. node
  113. && node.key.name === 'mixins'
  114. && hasPR
  115. );
  116. }
  117. /**
  118. * Mark shouldComponentUpdate as declared
  119. * @param {ASTNode} node The AST node being checked.
  120. */
  121. function markSCUAsDeclared(node) {
  122. components.set(node, {
  123. hasSCU: true,
  124. });
  125. }
  126. /**
  127. * Reports missing optimization for a given component
  128. * @param {Object} component The component to process
  129. */
  130. function reportMissingOptimization(component) {
  131. report(context, messages.noShouldComponentUpdate, 'noShouldComponentUpdate', {
  132. node: component.node,
  133. });
  134. }
  135. /**
  136. * Checks if we are declaring function in class
  137. * @returns {Boolean} True if we are declaring function in class, false if not.
  138. */
  139. function isFunctionInClass() {
  140. let blockNode;
  141. let scope = context.getScope();
  142. while (scope) {
  143. blockNode = scope.block;
  144. if (blockNode && blockNode.type === 'ClassDeclaration') {
  145. return true;
  146. }
  147. scope = scope.upper;
  148. }
  149. return false;
  150. }
  151. return {
  152. ArrowFunctionExpression(node) {
  153. // Skip if the function is declared in the class
  154. if (isFunctionInClass()) {
  155. return;
  156. }
  157. // Stateless Functional Components cannot be optimized (yet)
  158. markSCUAsDeclared(node);
  159. },
  160. ClassDeclaration(node) {
  161. if (!(
  162. hasPureRenderDecorator(node)
  163. || hasCustomDecorator(node)
  164. || componentUtil.isPureComponent(node, context)
  165. )) {
  166. return;
  167. }
  168. markSCUAsDeclared(node);
  169. },
  170. FunctionDeclaration(node) {
  171. // Skip if the function is declared in the class
  172. if (isFunctionInClass()) {
  173. return;
  174. }
  175. // Stateless Functional Components cannot be optimized (yet)
  176. markSCUAsDeclared(node);
  177. },
  178. FunctionExpression(node) {
  179. // Skip if the function is declared in the class
  180. if (isFunctionInClass()) {
  181. return;
  182. }
  183. // Stateless Functional Components cannot be optimized (yet)
  184. markSCUAsDeclared(node);
  185. },
  186. MethodDefinition(node) {
  187. if (!isSCUDeclared(node.key)) {
  188. return;
  189. }
  190. markSCUAsDeclared(node);
  191. },
  192. ObjectExpression(node) {
  193. // Search for the shouldComponentUpdate declaration
  194. const found = node.properties.some((property) => (
  195. property.key
  196. && (isSCUDeclared(property.key) || isPureRenderDeclared(property))
  197. ));
  198. if (found) {
  199. markSCUAsDeclared(node);
  200. }
  201. },
  202. 'Program:exit'() {
  203. // Report missing shouldComponentUpdate for all components
  204. values(components.list())
  205. .filter((component) => !component.hasSCU)
  206. .forEach((component) => {
  207. reportMissingOptimization(component);
  208. });
  209. },
  210. };
  211. }),
  212. };