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

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