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

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