require-optimization.js 6.6 KB

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