jsx-no-constructed-context-values.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. /**
  2. * @fileoverview Prevents jsx context provider values from taking values that
  3. * will cause needless rerenders.
  4. * @author Dylan Oshima
  5. */
  6. 'use strict';
  7. const Components = require('../util/Components');
  8. const docsUrl = require('../util/docsUrl');
  9. const getScope = require('../util/eslint').getScope;
  10. const report = require('../util/report');
  11. // ------------------------------------------------------------------------------
  12. // Helpers
  13. // ------------------------------------------------------------------------------
  14. // Recursively checks if an element is a construction.
  15. // A construction is a variable that changes identity every render.
  16. function isConstruction(node, callScope) {
  17. switch (node.type) {
  18. case 'Literal':
  19. if (node.regex != null) {
  20. return { type: 'regular expression', node };
  21. }
  22. return null;
  23. case 'Identifier': {
  24. const variableScoping = callScope.set.get(node.name);
  25. if (variableScoping == null || variableScoping.defs == null) {
  26. // If it's not in scope, we don't care.
  27. return null; // Handled
  28. }
  29. // Gets the last variable identity
  30. const variableDefs = variableScoping.defs;
  31. const def = variableDefs[variableDefs.length - 1];
  32. if (def != null
  33. && def.type !== 'Variable'
  34. && def.type !== 'FunctionName'
  35. ) {
  36. // Parameter or an unusual pattern. Bail out.
  37. return null; // Unhandled
  38. }
  39. if (def.node.type === 'FunctionDeclaration') {
  40. return { type: 'function declaration', node: def.node, usage: node };
  41. }
  42. const init = def.node.init;
  43. if (init == null) {
  44. return null;
  45. }
  46. const initConstruction = isConstruction(init, callScope);
  47. if (initConstruction == null) {
  48. return null;
  49. }
  50. return {
  51. type: initConstruction.type,
  52. node: initConstruction.node,
  53. usage: node,
  54. };
  55. }
  56. case 'ObjectExpression':
  57. // Any object initialized inline will create a new identity
  58. return { type: 'object', node };
  59. case 'ArrayExpression':
  60. return { type: 'array', node };
  61. case 'ArrowFunctionExpression':
  62. case 'FunctionExpression':
  63. // Functions that are initialized inline will have a new identity
  64. return { type: 'function expression', node };
  65. case 'ClassExpression':
  66. return { type: 'class expression', node };
  67. case 'NewExpression':
  68. // `const a = new SomeClass();` is a construction
  69. return { type: 'new expression', node };
  70. case 'ConditionalExpression':
  71. return (isConstruction(node.consequent, callScope)
  72. || isConstruction(node.alternate, callScope)
  73. );
  74. case 'LogicalExpression':
  75. return (isConstruction(node.left, callScope)
  76. || isConstruction(node.right, callScope)
  77. );
  78. case 'MemberExpression': {
  79. const objConstruction = isConstruction(node.object, callScope);
  80. if (objConstruction == null) {
  81. return null;
  82. }
  83. return {
  84. type: objConstruction.type,
  85. node: objConstruction.node,
  86. usage: node.object,
  87. };
  88. }
  89. case 'JSXFragment':
  90. return { type: 'JSX fragment', node };
  91. case 'JSXElement':
  92. return { type: 'JSX element', node };
  93. case 'AssignmentExpression': {
  94. const construct = isConstruction(node.right, callScope);
  95. if (construct != null) {
  96. return {
  97. type: 'assignment expression',
  98. node: construct.node,
  99. usage: node,
  100. };
  101. }
  102. return null;
  103. }
  104. case 'TypeCastExpression':
  105. case 'TSAsExpression':
  106. return isConstruction(node.expression, callScope);
  107. default:
  108. return null;
  109. }
  110. }
  111. function isReactContext(context, node) {
  112. let scope = getScope(context, node);
  113. let variableScoping = null;
  114. const contextName = node.name;
  115. while (scope && !variableScoping) { // Walk up the scope chain to find the variable
  116. variableScoping = scope.set.get(contextName);
  117. scope = scope.upper;
  118. }
  119. if (!variableScoping) { // Context was not found in scope
  120. return false;
  121. }
  122. // Get the variable's definition
  123. const def = variableScoping.defs[0];
  124. if (!def || def.node.type !== 'VariableDeclarator') {
  125. return false;
  126. }
  127. const init = def.node.init; // Variable initializer
  128. const isCreateContext = init
  129. && init.type === 'CallExpression'
  130. && (
  131. (
  132. init.callee.type === 'Identifier'
  133. && init.callee.name === 'createContext'
  134. ) || (
  135. init.callee.type === 'MemberExpression'
  136. && init.callee.object.name === 'React'
  137. && init.callee.property.name === 'createContext'
  138. )
  139. );
  140. return isCreateContext;
  141. }
  142. // ------------------------------------------------------------------------------
  143. // Rule Definition
  144. // ------------------------------------------------------------------------------
  145. const messages = {
  146. 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.",
  147. 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.",
  148. 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.',
  149. 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.',
  150. };
  151. /** @type {import('eslint').Rule.RuleModule} */
  152. module.exports = {
  153. meta: {
  154. docs: {
  155. description: 'Disallows JSX context provider values from taking values that will cause needless rerenders',
  156. category: 'Best Practices',
  157. recommended: false,
  158. url: docsUrl('jsx-no-constructed-context-values'),
  159. },
  160. messages,
  161. schema: false,
  162. },
  163. // eslint-disable-next-line arrow-body-style
  164. create: Components.detect((context, components, utils) => {
  165. return {
  166. JSXOpeningElement(node) {
  167. const openingElementName = node.name;
  168. if (openingElementName.type === 'JSXMemberExpression') {
  169. const isJSXContext = openingElementName.property.name === 'Provider';
  170. if (!isJSXContext) {
  171. // Member is not Provider
  172. return;
  173. }
  174. } else if (openingElementName.type === 'JSXIdentifier') {
  175. const isJSXContext = isReactContext(context, openingElementName);
  176. if (!isJSXContext) {
  177. // Member is not context
  178. return;
  179. }
  180. } else {
  181. return;
  182. }
  183. // Contexts can take in more than just a value prop
  184. // so we need to iterate through all of them
  185. const jsxValueAttribute = node.attributes.find(
  186. (attribute) => attribute.type === 'JSXAttribute' && attribute.name.name === 'value'
  187. );
  188. if (jsxValueAttribute == null) {
  189. // No value prop was passed
  190. return;
  191. }
  192. const valueNode = jsxValueAttribute.value;
  193. if (!valueNode) {
  194. // attribute is a boolean shorthand
  195. return;
  196. }
  197. if (valueNode.type !== 'JSXExpressionContainer') {
  198. // value could be a literal
  199. return;
  200. }
  201. const valueExpression = valueNode.expression;
  202. const invocationScope = getScope(context, node);
  203. // Check if the value prop is a construction
  204. const constructInfo = isConstruction(valueExpression, invocationScope);
  205. if (constructInfo == null) {
  206. return;
  207. }
  208. if (!utils.getParentComponent(node)) {
  209. return;
  210. }
  211. // Report found error
  212. const constructType = constructInfo.type;
  213. const constructNode = constructInfo.node;
  214. const constructUsage = constructInfo.usage;
  215. const data = {
  216. type: constructType, nodeLine: constructNode.loc.start.line,
  217. };
  218. let messageId = 'defaultMsg';
  219. // Variable passed to value prop
  220. if (constructUsage != null) {
  221. messageId = 'withIdentifierMsg';
  222. data.usageLine = constructUsage.loc.start.line;
  223. data.variableName = constructUsage.name;
  224. }
  225. // Type of expression
  226. if (
  227. constructType === 'function expression'
  228. || constructType === 'function declaration'
  229. ) {
  230. messageId += 'Func';
  231. }
  232. report(context, messages[messageId], messageId, {
  233. node: constructNode,
  234. data,
  235. });
  236. },
  237. };
  238. }),
  239. };