/** * @fileoverview Enforce React components to have a shouldComponentUpdate method * @author Evgueni Naverniouk */ 'use strict'; const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); module.exports = { meta: { docs: { description: 'Enforce React components to have a shouldComponentUpdate method', category: 'Best Practices', recommended: false, url: docsUrl('require-optimization') }, schema: [{ type: 'object', properties: { allowDecorators: { type: 'array', items: { type: 'string' } } }, additionalProperties: false }] }, create: Components.detect((context, components, utils) => { const MISSING_MESSAGE = 'Component is not optimized. Please add a shouldComponentUpdate method.'; const configuration = context.options[0] || {}; const allowDecorators = configuration.allowDecorators || []; /** * Checks to see if our component is decorated by PureRenderMixin via reactMixin * @param {ASTNode} node The AST node being checked. * @returns {Boolean} True if node is decorated with a PureRenderMixin, false if not. */ const hasPureRenderDecorator = function (node) { if (node.decorators && node.decorators.length) { for (let i = 0, l = node.decorators.length; i < l; i++) { if ( node.decorators[i].expression && node.decorators[i].expression.callee && node.decorators[i].expression.callee.object && node.decorators[i].expression.callee.object.name === 'reactMixin' && node.decorators[i].expression.callee.property && node.decorators[i].expression.callee.property.name === 'decorate' && node.decorators[i].expression.arguments && node.decorators[i].expression.arguments.length && node.decorators[i].expression.arguments[0].name === 'PureRenderMixin' ) { return true; } } } return false; }; /** * Checks to see if our component is custom decorated * @param {ASTNode} node The AST node being checked. * @returns {Boolean} True if node is decorated name with a custom decorated, false if not. */ const hasCustomDecorator = function (node) { const allowLength = allowDecorators.length; if (allowLength && node.decorators && node.decorators.length) { for (let i = 0; i < allowLength; i++) { for (let j = 0, l = node.decorators.length; j < l; j++) { if ( node.decorators[j].expression && node.decorators[j].expression.name === allowDecorators[i] ) { return true; } } } } return false; }; /** * Checks if we are declaring a shouldComponentUpdate method * @param {ASTNode} node The AST node being checked. * @returns {Boolean} True if we are declaring a shouldComponentUpdate method, false if not. */ const isSCUDeclared = function (node) { return Boolean( node && node.name === 'shouldComponentUpdate' ); }; /** * Checks if we are declaring a PureRenderMixin mixin * @param {ASTNode} node The AST node being checked. * @returns {Boolean} True if we are declaring a PureRenderMixin method, false if not. */ const isPureRenderDeclared = function (node) { let hasPR = false; if (node.value && node.value.elements) { for (let i = 0, l = node.value.elements.length; i < l; i++) { if (node.value.elements[i] && node.value.elements[i].name === 'PureRenderMixin') { hasPR = true; break; } } } return Boolean( node && node.key.name === 'mixins' && hasPR ); }; /** * Mark shouldComponentUpdate as declared * @param {ASTNode} node The AST node being checked. */ const markSCUAsDeclared = function (node) { components.set(node, { hasSCU: true }); }; /** * Reports missing optimization for a given component * @param {Object} component The component to process */ const reportMissingOptimization = function (component) { context.report({ node: component.node, message: MISSING_MESSAGE, data: { component: component.name } }); }; /** * Checks if we are declaring function in class * @returns {Boolean} True if we are declaring function in class, false if not. */ const isFunctionInClass = function () { let blockNode; let scope = context.getScope(); while (scope) { blockNode = scope.block; if (blockNode && blockNode.type === 'ClassDeclaration') { return true; } scope = scope.upper; } return false; }; return { ArrowFunctionExpression(node) { // Stateless Functional Components cannot be optimized (yet) markSCUAsDeclared(node); }, ClassDeclaration(node) { if (!(hasPureRenderDecorator(node) || hasCustomDecorator(node) || utils.isPureComponent(node))) { return; } markSCUAsDeclared(node); }, FunctionDeclaration(node) { // Skip if the function is declared in the class if (isFunctionInClass()) { return; } // Stateless Functional Components cannot be optimized (yet) markSCUAsDeclared(node); }, FunctionExpression(node) { // Skip if the function is declared in the class if (isFunctionInClass()) { return; } // Stateless Functional Components cannot be optimized (yet) markSCUAsDeclared(node); }, MethodDefinition(node) { if (!isSCUDeclared(node.key)) { return; } markSCUAsDeclared(node); }, ObjectExpression(node) { // Search for the shouldComponentUpdate declaration const found = node.properties.some(property => ( property.key && (isSCUDeclared(property.key) || isPureRenderDeclared(property)) )); if (found) { markSCUAsDeclared(node); } }, 'Program:exit': function () { const list = components.list(); // Report missing shouldComponentUpdate for all components Object.keys(list).filter(component => !list[component].hasSCU).forEach((component) => { reportMissingOptimization(list[component]); }); } }; }) };