228 lines
7.4 KiB
JavaScript
228 lines
7.4 KiB
JavaScript
/**
|
|
* @fileoverview Prevents jsx context provider values from taking values that
|
|
* will cause needless rerenders.
|
|
* @author Dylan Oshima
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const Components = require('../util/Components');
|
|
const docsUrl = require('../util/docsUrl');
|
|
const getScope = require('../util/eslint').getScope;
|
|
const report = require('../util/report');
|
|
|
|
// ------------------------------------------------------------------------------
|
|
// Helpers
|
|
// ------------------------------------------------------------------------------
|
|
|
|
// Recursively checks if an element is a construction.
|
|
// A construction is a variable that changes identity every render.
|
|
function isConstruction(node, callScope) {
|
|
switch (node.type) {
|
|
case 'Literal':
|
|
if (node.regex != null) {
|
|
return { type: 'regular expression', node };
|
|
}
|
|
return null;
|
|
case 'Identifier': {
|
|
const variableScoping = callScope.set.get(node.name);
|
|
|
|
if (variableScoping == null || variableScoping.defs == null) {
|
|
// If it's not in scope, we don't care.
|
|
return null; // Handled
|
|
}
|
|
|
|
// Gets the last variable identity
|
|
const variableDefs = variableScoping.defs;
|
|
const def = variableDefs[variableDefs.length - 1];
|
|
if (def != null
|
|
&& def.type !== 'Variable'
|
|
&& def.type !== 'FunctionName'
|
|
) {
|
|
// Parameter or an unusual pattern. Bail out.
|
|
return null; // Unhandled
|
|
}
|
|
|
|
if (def.node.type === 'FunctionDeclaration') {
|
|
return { type: 'function declaration', node: def.node, usage: node };
|
|
}
|
|
|
|
const init = def.node.init;
|
|
if (init == null) {
|
|
return null;
|
|
}
|
|
|
|
const initConstruction = isConstruction(init, callScope);
|
|
if (initConstruction == null) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
type: initConstruction.type,
|
|
node: initConstruction.node,
|
|
usage: node,
|
|
};
|
|
}
|
|
case 'ObjectExpression':
|
|
// Any object initialized inline will create a new identity
|
|
return { type: 'object', node };
|
|
case 'ArrayExpression':
|
|
return { type: 'array', node };
|
|
case 'ArrowFunctionExpression':
|
|
case 'FunctionExpression':
|
|
// Functions that are initialized inline will have a new identity
|
|
return { type: 'function expression', node };
|
|
case 'ClassExpression':
|
|
return { type: 'class expression', node };
|
|
case 'NewExpression':
|
|
// `const a = new SomeClass();` is a construction
|
|
return { type: 'new expression', node };
|
|
case 'ConditionalExpression':
|
|
return (isConstruction(node.consequent, callScope)
|
|
|| isConstruction(node.alternate, callScope)
|
|
);
|
|
case 'LogicalExpression':
|
|
return (isConstruction(node.left, callScope)
|
|
|| isConstruction(node.right, callScope)
|
|
);
|
|
case 'MemberExpression': {
|
|
const objConstruction = isConstruction(node.object, callScope);
|
|
if (objConstruction == null) {
|
|
return null;
|
|
}
|
|
return {
|
|
type: objConstruction.type,
|
|
node: objConstruction.node,
|
|
usage: node.object,
|
|
};
|
|
}
|
|
case 'JSXFragment':
|
|
return { type: 'JSX fragment', node };
|
|
case 'JSXElement':
|
|
return { type: 'JSX element', node };
|
|
case 'AssignmentExpression': {
|
|
const construct = isConstruction(node.right, callScope);
|
|
if (construct != null) {
|
|
return {
|
|
type: 'assignment expression',
|
|
node: construct.node,
|
|
usage: node,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
case 'TypeCastExpression':
|
|
case 'TSAsExpression':
|
|
return isConstruction(node.expression, callScope);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------
|
|
// Rule Definition
|
|
// ------------------------------------------------------------------------------
|
|
|
|
const messages = {
|
|
withIdentifierMsg: "The '{{variableName}}' {{type}} (at line {{nodeLine}}) passed as the value prop to the Context provider (at line {{usageLine}}) changes every render. To fix this consider wrapping it in a useMemo hook.",
|
|
withIdentifierMsgFunc: "The '{{variableName}}' {{type}} (at line {{nodeLine}}) passed as the value prop to the Context provider (at line {{usageLine}}) changes every render. To fix this consider wrapping it in a useCallback hook.",
|
|
defaultMsg: 'The {{type}} passed as the value prop to the Context provider (at line {{nodeLine}}) changes every render. To fix this consider wrapping it in a useMemo hook.',
|
|
defaultMsgFunc: 'The {{type}} passed as the value prop to the Context provider (at line {{nodeLine}}) changes every render. To fix this consider wrapping it in a useCallback hook.',
|
|
};
|
|
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
module.exports = {
|
|
meta: {
|
|
docs: {
|
|
description: 'Disallows JSX context provider values from taking values that will cause needless rerenders',
|
|
category: 'Best Practices',
|
|
recommended: false,
|
|
url: docsUrl('jsx-no-constructed-context-values'),
|
|
},
|
|
messages,
|
|
schema: false,
|
|
},
|
|
|
|
// eslint-disable-next-line arrow-body-style
|
|
create: Components.detect((context, components, utils) => {
|
|
return {
|
|
JSXOpeningElement(node) {
|
|
const openingElementName = node.name;
|
|
if (openingElementName.type !== 'JSXMemberExpression') {
|
|
// Has no member
|
|
return;
|
|
}
|
|
|
|
const isJsxContext = openingElementName.property.name === 'Provider';
|
|
if (!isJsxContext) {
|
|
// Member is not Provider
|
|
return;
|
|
}
|
|
|
|
// Contexts can take in more than just a value prop
|
|
// so we need to iterate through all of them
|
|
const jsxValueAttribute = node.attributes.find(
|
|
(attribute) => attribute.type === 'JSXAttribute' && attribute.name.name === 'value'
|
|
);
|
|
|
|
if (jsxValueAttribute == null) {
|
|
// No value prop was passed
|
|
return;
|
|
}
|
|
|
|
const valueNode = jsxValueAttribute.value;
|
|
if (!valueNode) {
|
|
// attribute is a boolean shorthand
|
|
return;
|
|
}
|
|
if (valueNode.type !== 'JSXExpressionContainer') {
|
|
// value could be a literal
|
|
return;
|
|
}
|
|
|
|
const valueExpression = valueNode.expression;
|
|
const invocationScope = getScope(context, node);
|
|
|
|
// Check if the value prop is a construction
|
|
const constructInfo = isConstruction(valueExpression, invocationScope);
|
|
if (constructInfo == null) {
|
|
return;
|
|
}
|
|
|
|
if (!utils.getParentComponent(node)) {
|
|
return;
|
|
}
|
|
|
|
// Report found error
|
|
const constructType = constructInfo.type;
|
|
const constructNode = constructInfo.node;
|
|
const constructUsage = constructInfo.usage;
|
|
const data = {
|
|
type: constructType, nodeLine: constructNode.loc.start.line,
|
|
};
|
|
let messageId = 'defaultMsg';
|
|
|
|
// Variable passed to value prop
|
|
if (constructUsage != null) {
|
|
messageId = 'withIdentifierMsg';
|
|
data.usageLine = constructUsage.loc.start.line;
|
|
data.variableName = constructUsage.name;
|
|
}
|
|
|
|
// Type of expression
|
|
if (
|
|
constructType === 'function expression'
|
|
|| constructType === 'function declaration'
|
|
) {
|
|
messageId += 'Func';
|
|
}
|
|
|
|
report(context, messages[messageId], messageId, {
|
|
node: constructNode,
|
|
data,
|
|
});
|
|
},
|
|
};
|
|
}),
|
|
};
|