require-optimization.js 6.8 KB

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