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
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, |
|
}); |
|
}, |
|
}; |
|
}, |
|
};
|
|
|