blob: 612f492cea2780f6967c7ba0d05fee9f74463fac [file] [log] [blame]
/**
* @fileoverview Prevents usage of Function.prototype.bind and arrow functions
* in React component props.
* @author Daniel Lo Nigro <dan.cx>
* @author Jacky Ho
*/
'use strict';
const propName = require('jsx-ast-utils/propName');
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
const violationMessageStore = {
bindCall: 'JSX props should not use .bind()',
arrowFunc: 'JSX props should not use arrow functions',
bindExpression: 'JSX props should not use ::',
func: 'JSX props should not use functions'
};
module.exports = {
meta: {
docs: {
description: 'Prevents usage of Function.prototype.bind and arrow functions in React component props',
category: 'Best Practices',
recommended: false,
url: docsUrl('jsx-no-bind')
},
schema: [{
type: 'object',
properties: {
allowArrowFunctions: {
default: false,
type: 'boolean'
},
allowBind: {
default: false,
type: 'boolean'
},
allowFunctions: {
default: false,
type: 'boolean'
},
ignoreRefs: {
default: false,
type: 'boolean'
},
ignoreDOMComponents: {
default: false,
type: 'boolean'
}
},
additionalProperties: false
}]
},
create: Components.detect((context) => {
const configuration = context.options[0] || {};
// Keep track of all the variable names pointing to a bind call,
// bind expression or an arrow function in different block statements
const blockVariableNameSets = {};
function setBlockVariableNameSet(blockStart) {
blockVariableNameSets[blockStart] = {
arrowFunc: new Set(),
bindCall: new Set(),
bindExpression: new Set(),
func: new Set()
};
}
function getNodeViolationType(node) {
const nodeType = node.type;
if (
!configuration.allowBind &&
nodeType === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'bind'
) {
return 'bindCall';
}
if (nodeType === 'ConditionalExpression') {
return getNodeViolationType(node.test) ||
getNodeViolationType(node.consequent) ||
getNodeViolationType(node.alternate);
}
if (!configuration.allowArrowFunctions && nodeType === 'ArrowFunctionExpression') {
return 'arrowFunc';
}
if (!configuration.allowFunctions && nodeType === 'FunctionExpression') {
return 'func';
}
if (!configuration.allowBind && nodeType === 'BindExpression') {
return 'bindExpression';
}
return null;
}
function addVariableNameToSet(violationType, variableName, blockStart) {
blockVariableNameSets[blockStart][violationType].add(variableName);
}
function getBlockStatementAncestors(node) {
return context.getAncestors(node).reverse().filter(
ancestor => ancestor.type === 'BlockStatement'
);
}
function reportVariableViolation(node, name, blockStart) {
const blockSets = blockVariableNameSets[blockStart];
const violationTypes = Object.keys(blockSets);
return violationTypes.find((type) => {
if (blockSets[type].has(name)) {
context.report({node, message: violationMessageStore[type]});
return true;
}
return false;
});
}
function findVariableViolation(node, name) {
getBlockStatementAncestors(node).find(
block => reportVariableViolation(node, name, block.start)
);
}
return {
BlockStatement(node) {
setBlockVariableNameSet(node.start);
},
VariableDeclarator(node) {
if (!node.init) {
return;
}
const blockAncestors = getBlockStatementAncestors(node);
const variableViolationType = getNodeViolationType(node.init);
if (
blockAncestors.length > 0 &&
variableViolationType &&
node.parent.kind === 'const' // only support const right now
) {
addVariableNameToSet(
variableViolationType, node.id.name, blockAncestors[0].start
);
}
},
JSXAttribute(node) {
const isRef = configuration.ignoreRefs && propName(node) === 'ref';
if (isRef || !node.value || !node.value.expression) {
return;
}
const isDOMComponent = jsxUtil.isDOMComponent(node.parent);
if (configuration.ignoreDOMComponents && isDOMComponent) {
return;
}
const valueNode = node.value.expression;
const valueNodeType = valueNode.type;
const nodeViolationType = getNodeViolationType(valueNode);
if (valueNodeType === 'Identifier') {
findVariableViolation(node, valueNode.name);
} else if (nodeViolationType) {
context.report({
node, message: violationMessageStore[nodeViolationType]
});
}
}
};
})
};