blob: 329354581d97fdb5112f86a2246514945f9af01c [file] [log] [blame]
/**
* @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]);
});
}
};
})
};