You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

218 lines
7.0 KiB

/**
* @fileoverview Prevents jsx context provider values from taking values that
* will cause needless rerenders.
* @author Dylan Oshima
*/
'use strict';
const docsUrl = require('../util/docsUrl');
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.',
};
module.exports = {
meta: {
docs: {
description: 'Prevents 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,
},
create(context) {
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 = context.getScope();
// Check if the value prop is a construction
const constructInfo = isConstruction(valueExpression, invocationScope);
if (constructInfo == null) {
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,
});
},
};
},
};