123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- /**
- * @fileoverview Prevents jsx context provider values from taking values that
- * will cause needless rerenders.
- * @author Dylan Oshima
- */
- 'use strict';
- const docsUrl = require('../util/docsUrl');
- // ------------------------------------------------------------------------------
- // 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
- // ------------------------------------------------------------------------------
- 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: {
- 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.'
- }
- },
- 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';
- }
- context.report({
- node: constructNode,
- messageId,
- data
- });
- }
- };
- }
- };
|