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

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