eslint-plugin-react-hooks.development.js 86 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458
  1. /**
  2. * @license React
  3. * eslint-plugin-react-hooks.development.js
  4. *
  5. * Copyright (c) Facebook, Inc. and its affiliates.
  6. *
  7. * This source code is licensed under the MIT license found in the
  8. * LICENSE file in the root directory of this source tree.
  9. */
  10. 'use strict';
  11. if (process.env.NODE_ENV !== "production") {
  12. (function() {
  13. 'use strict';
  14. function _unsupportedIterableToArray(o, minLen) {
  15. if (!o) return;
  16. if (typeof o === "string") return _arrayLikeToArray(o, minLen);
  17. var n = Object.prototype.toString.call(o).slice(8, -1);
  18. if (n === "Object" && o.constructor) n = o.constructor.name;
  19. if (n === "Map" || n === "Set") return Array.from(o);
  20. if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
  21. }
  22. function _arrayLikeToArray(arr, len) {
  23. if (len == null || len > arr.length) len = arr.length;
  24. for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
  25. return arr2;
  26. }
  27. function _createForOfIteratorHelper(o, allowArrayLike) {
  28. var it;
  29. if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) {
  30. if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
  31. if (it) o = it;
  32. var i = 0;
  33. var F = function () {};
  34. return {
  35. s: F,
  36. n: function () {
  37. if (i >= o.length) return {
  38. done: true
  39. };
  40. return {
  41. done: false,
  42. value: o[i++]
  43. };
  44. },
  45. e: function (e) {
  46. throw e;
  47. },
  48. f: F
  49. };
  50. }
  51. throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
  52. }
  53. var normalCompletion = true,
  54. didErr = false,
  55. err;
  56. return {
  57. s: function () {
  58. it = o[Symbol.iterator]();
  59. },
  60. n: function () {
  61. var step = it.next();
  62. normalCompletion = step.done;
  63. return step;
  64. },
  65. e: function (e) {
  66. didErr = true;
  67. err = e;
  68. },
  69. f: function () {
  70. try {
  71. if (!normalCompletion && it.return != null) it.return();
  72. } finally {
  73. if (didErr) throw err;
  74. }
  75. }
  76. };
  77. }
  78. /* global BigInt */
  79. function isHookName(s) {
  80. return /^use[A-Z0-9].*$/.test(s);
  81. }
  82. /**
  83. * We consider hooks to be a hook name identifier or a member expression
  84. * containing a hook name.
  85. */
  86. function isHook(node) {
  87. if (node.type === 'Identifier') {
  88. return isHookName(node.name);
  89. } else if (node.type === 'MemberExpression' && !node.computed && isHook(node.property)) {
  90. var obj = node.object;
  91. var isPascalCaseNameSpace = /^[A-Z].*/;
  92. return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name);
  93. } else {
  94. return false;
  95. }
  96. }
  97. /**
  98. * Checks if the node is a React component name. React component names must
  99. * always start with a non-lowercase letter. So `MyComponent` or `_MyComponent`
  100. * are valid component names for instance.
  101. */
  102. function isComponentName(node) {
  103. if (node.type === 'Identifier') {
  104. return !/^[a-z]/.test(node.name);
  105. } else {
  106. return false;
  107. }
  108. }
  109. function isReactFunction(node, functionName) {
  110. return node.name === functionName || node.type === 'MemberExpression' && node.object.name === 'React' && node.property.name === functionName;
  111. }
  112. /**
  113. * Checks if the node is a callback argument of forwardRef. This render function
  114. * should follow the rules of hooks.
  115. */
  116. function isForwardRefCallback(node) {
  117. return !!(node.parent && node.parent.callee && isReactFunction(node.parent.callee, 'forwardRef'));
  118. }
  119. /**
  120. * Checks if the node is a callback argument of React.memo. This anonymous
  121. * functional component should follow the rules of hooks.
  122. */
  123. function isMemoCallback(node) {
  124. return !!(node.parent && node.parent.callee && isReactFunction(node.parent.callee, 'memo'));
  125. }
  126. function isInsideComponentOrHook(node) {
  127. while (node) {
  128. var functionName = getFunctionName(node);
  129. if (functionName) {
  130. if (isComponentName(functionName) || isHook(functionName)) {
  131. return true;
  132. }
  133. }
  134. if (isForwardRefCallback(node) || isMemoCallback(node)) {
  135. return true;
  136. }
  137. node = node.parent;
  138. }
  139. return false;
  140. }
  141. var RulesOfHooks = {
  142. meta: {
  143. type: 'problem',
  144. docs: {
  145. description: 'enforces the Rules of Hooks',
  146. recommended: true,
  147. url: 'https://reactjs.org/docs/hooks-rules.html'
  148. }
  149. },
  150. create: function (context) {
  151. var codePathReactHooksMapStack = [];
  152. var codePathSegmentStack = [];
  153. return {
  154. // Maintain code segment path stack as we traverse.
  155. onCodePathSegmentStart: function (segment) {
  156. return codePathSegmentStack.push(segment);
  157. },
  158. onCodePathSegmentEnd: function () {
  159. return codePathSegmentStack.pop();
  160. },
  161. // Maintain code path stack as we traverse.
  162. onCodePathStart: function () {
  163. return codePathReactHooksMapStack.push(new Map());
  164. },
  165. // Process our code path.
  166. //
  167. // Everything is ok if all React Hooks are both reachable from the initial
  168. // segment and reachable from every final segment.
  169. onCodePathEnd: function (codePath, codePathNode) {
  170. var reactHooksMap = codePathReactHooksMapStack.pop();
  171. if (reactHooksMap.size === 0) {
  172. return;
  173. } // All of the segments which are cyclic are recorded in this set.
  174. var cyclic = new Set();
  175. /**
  176. * Count the number of code paths from the start of the function to this
  177. * segment. For example:
  178. *
  179. * ```js
  180. * function MyComponent() {
  181. * if (condition) {
  182. * // Segment 1
  183. * } else {
  184. * // Segment 2
  185. * }
  186. * // Segment 3
  187. * }
  188. * ```
  189. *
  190. * Segments 1 and 2 have one path to the beginning of `MyComponent` and
  191. * segment 3 has two paths to the beginning of `MyComponent` since we
  192. * could have either taken the path of segment 1 or segment 2.
  193. *
  194. * Populates `cyclic` with cyclic segments.
  195. */
  196. function countPathsFromStart(segment, pathHistory) {
  197. var cache = countPathsFromStart.cache;
  198. var paths = cache.get(segment.id);
  199. var pathList = new Set(pathHistory); // If `pathList` includes the current segment then we've found a cycle!
  200. // We need to fill `cyclic` with all segments inside cycle
  201. if (pathList.has(segment.id)) {
  202. var pathArray = [].concat(pathList);
  203. var cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
  204. var _iterator = _createForOfIteratorHelper(cyclicSegments),
  205. _step;
  206. try {
  207. for (_iterator.s(); !(_step = _iterator.n()).done;) {
  208. var cyclicSegment = _step.value;
  209. cyclic.add(cyclicSegment);
  210. }
  211. } catch (err) {
  212. _iterator.e(err);
  213. } finally {
  214. _iterator.f();
  215. }
  216. return BigInt('0');
  217. } // add the current segment to pathList
  218. pathList.add(segment.id); // We have a cached `paths`. Return it.
  219. if (paths !== undefined) {
  220. return paths;
  221. }
  222. if (codePath.thrownSegments.includes(segment)) {
  223. paths = BigInt('0');
  224. } else if (segment.prevSegments.length === 0) {
  225. paths = BigInt('1');
  226. } else {
  227. paths = BigInt('0');
  228. var _iterator2 = _createForOfIteratorHelper(segment.prevSegments),
  229. _step2;
  230. try {
  231. for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
  232. var prevSegment = _step2.value;
  233. paths += countPathsFromStart(prevSegment, pathList);
  234. }
  235. } catch (err) {
  236. _iterator2.e(err);
  237. } finally {
  238. _iterator2.f();
  239. }
  240. } // If our segment is reachable then there should be at least one path
  241. // to it from the start of our code path.
  242. if (segment.reachable && paths === BigInt('0')) {
  243. cache.delete(segment.id);
  244. } else {
  245. cache.set(segment.id, paths);
  246. }
  247. return paths;
  248. }
  249. /**
  250. * Count the number of code paths from this segment to the end of the
  251. * function. For example:
  252. *
  253. * ```js
  254. * function MyComponent() {
  255. * // Segment 1
  256. * if (condition) {
  257. * // Segment 2
  258. * } else {
  259. * // Segment 3
  260. * }
  261. * }
  262. * ```
  263. *
  264. * Segments 2 and 3 have one path to the end of `MyComponent` and
  265. * segment 1 has two paths to the end of `MyComponent` since we could
  266. * either take the path of segment 1 or segment 2.
  267. *
  268. * Populates `cyclic` with cyclic segments.
  269. */
  270. function countPathsToEnd(segment, pathHistory) {
  271. var cache = countPathsToEnd.cache;
  272. var paths = cache.get(segment.id);
  273. var pathList = new Set(pathHistory); // If `pathList` includes the current segment then we've found a cycle!
  274. // We need to fill `cyclic` with all segments inside cycle
  275. if (pathList.has(segment.id)) {
  276. var pathArray = Array.from(pathList);
  277. var cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
  278. var _iterator3 = _createForOfIteratorHelper(cyclicSegments),
  279. _step3;
  280. try {
  281. for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
  282. var cyclicSegment = _step3.value;
  283. cyclic.add(cyclicSegment);
  284. }
  285. } catch (err) {
  286. _iterator3.e(err);
  287. } finally {
  288. _iterator3.f();
  289. }
  290. return BigInt('0');
  291. } // add the current segment to pathList
  292. pathList.add(segment.id); // We have a cached `paths`. Return it.
  293. if (paths !== undefined) {
  294. return paths;
  295. }
  296. if (codePath.thrownSegments.includes(segment)) {
  297. paths = BigInt('0');
  298. } else if (segment.nextSegments.length === 0) {
  299. paths = BigInt('1');
  300. } else {
  301. paths = BigInt('0');
  302. var _iterator4 = _createForOfIteratorHelper(segment.nextSegments),
  303. _step4;
  304. try {
  305. for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
  306. var nextSegment = _step4.value;
  307. paths += countPathsToEnd(nextSegment, pathList);
  308. }
  309. } catch (err) {
  310. _iterator4.e(err);
  311. } finally {
  312. _iterator4.f();
  313. }
  314. }
  315. cache.set(segment.id, paths);
  316. return paths;
  317. }
  318. /**
  319. * Gets the shortest path length to the start of a code path.
  320. * For example:
  321. *
  322. * ```js
  323. * function MyComponent() {
  324. * if (condition) {
  325. * // Segment 1
  326. * }
  327. * // Segment 2
  328. * }
  329. * ```
  330. *
  331. * There is only one path from segment 1 to the code path start. Its
  332. * length is one so that is the shortest path.
  333. *
  334. * There are two paths from segment 2 to the code path start. One
  335. * through segment 1 with a length of two and another directly to the
  336. * start with a length of one. The shortest path has a length of one
  337. * so we would return that.
  338. */
  339. function shortestPathLengthToStart(segment) {
  340. var cache = shortestPathLengthToStart.cache;
  341. var length = cache.get(segment.id); // If `length` is null then we found a cycle! Return infinity since
  342. // the shortest path is definitely not the one where we looped.
  343. if (length === null) {
  344. return Infinity;
  345. } // We have a cached `length`. Return it.
  346. if (length !== undefined) {
  347. return length;
  348. } // Compute `length` and cache it. Guarding against cycles.
  349. cache.set(segment.id, null);
  350. if (segment.prevSegments.length === 0) {
  351. length = 1;
  352. } else {
  353. length = Infinity;
  354. var _iterator5 = _createForOfIteratorHelper(segment.prevSegments),
  355. _step5;
  356. try {
  357. for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
  358. var prevSegment = _step5.value;
  359. var prevLength = shortestPathLengthToStart(prevSegment);
  360. if (prevLength < length) {
  361. length = prevLength;
  362. }
  363. }
  364. } catch (err) {
  365. _iterator5.e(err);
  366. } finally {
  367. _iterator5.f();
  368. }
  369. length += 1;
  370. }
  371. cache.set(segment.id, length);
  372. return length;
  373. }
  374. countPathsFromStart.cache = new Map();
  375. countPathsToEnd.cache = new Map();
  376. shortestPathLengthToStart.cache = new Map(); // Count all code paths to the end of our component/hook. Also primes
  377. // the `countPathsToEnd` cache.
  378. var allPathsFromStartToEnd = countPathsToEnd(codePath.initialSegment); // Gets the function name for our code path. If the function name is
  379. // `undefined` then we know either that we have an anonymous function
  380. // expression or our code path is not in a function. In both cases we
  381. // will want to error since neither are React function components or
  382. // hook functions - unless it is an anonymous function argument to
  383. // forwardRef or memo.
  384. var codePathFunctionName = getFunctionName(codePathNode); // This is a valid code path for React hooks if we are directly in a React
  385. // function component or we are in a hook function.
  386. var isSomewhereInsideComponentOrHook = isInsideComponentOrHook(codePathNode);
  387. var isDirectlyInsideComponentOrHook = codePathFunctionName ? isComponentName(codePathFunctionName) || isHook(codePathFunctionName) : isForwardRefCallback(codePathNode) || isMemoCallback(codePathNode); // Compute the earliest finalizer level using information from the
  388. // cache. We expect all reachable final segments to have a cache entry
  389. // after calling `visitSegment()`.
  390. var shortestFinalPathLength = Infinity;
  391. var _iterator6 = _createForOfIteratorHelper(codePath.finalSegments),
  392. _step6;
  393. try {
  394. for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
  395. var finalSegment = _step6.value;
  396. if (!finalSegment.reachable) {
  397. continue;
  398. }
  399. var length = shortestPathLengthToStart(finalSegment);
  400. if (length < shortestFinalPathLength) {
  401. shortestFinalPathLength = length;
  402. }
  403. } // Make sure all React Hooks pass our lint invariants. Log warnings
  404. // if not.
  405. } catch (err) {
  406. _iterator6.e(err);
  407. } finally {
  408. _iterator6.f();
  409. }
  410. var _iterator7 = _createForOfIteratorHelper(reactHooksMap),
  411. _step7;
  412. try {
  413. for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
  414. var _step7$value = _step7.value,
  415. segment = _step7$value[0],
  416. reactHooks = _step7$value[1];
  417. // NOTE: We could report here that the hook is not reachable, but
  418. // that would be redundant with more general "no unreachable"
  419. // lint rules.
  420. if (!segment.reachable) {
  421. continue;
  422. } // If there are any final segments with a shorter path to start then
  423. // we possibly have an early return.
  424. //
  425. // If our segment is a final segment itself then siblings could
  426. // possibly be early returns.
  427. var possiblyHasEarlyReturn = segment.nextSegments.length === 0 ? shortestFinalPathLength <= shortestPathLengthToStart(segment) : shortestFinalPathLength < shortestPathLengthToStart(segment); // Count all the paths from the start of our code path to the end of
  428. // our code path that go _through_ this segment. The critical piece
  429. // of this is _through_. If we just call `countPathsToEnd(segment)`
  430. // then we neglect that we may have gone through multiple paths to get
  431. // to this point! Consider:
  432. //
  433. // ```js
  434. // function MyComponent() {
  435. // if (a) {
  436. // // Segment 1
  437. // } else {
  438. // // Segment 2
  439. // }
  440. // // Segment 3
  441. // if (b) {
  442. // // Segment 4
  443. // } else {
  444. // // Segment 5
  445. // }
  446. // }
  447. // ```
  448. //
  449. // In this component we have four code paths:
  450. //
  451. // 1. `a = true; b = true`
  452. // 2. `a = true; b = false`
  453. // 3. `a = false; b = true`
  454. // 4. `a = false; b = false`
  455. //
  456. // From segment 3 there are two code paths to the end through segment
  457. // 4 and segment 5. However, we took two paths to get here through
  458. // segment 1 and segment 2.
  459. //
  460. // If we multiply the paths from start (two) by the paths to end (two)
  461. // for segment 3 we get four. Which is our desired count.
  462. var pathsFromStartToEnd = countPathsFromStart(segment) * countPathsToEnd(segment); // Is this hook a part of a cyclic segment?
  463. var cycled = cyclic.has(segment.id);
  464. var _iterator8 = _createForOfIteratorHelper(reactHooks),
  465. _step8;
  466. try {
  467. for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
  468. var hook = _step8.value;
  469. // Report an error if a hook may be called more then once.
  470. if (cycled) {
  471. context.report({
  472. node: hook,
  473. message: "React Hook \"" + context.getSource(hook) + "\" may be executed " + 'more than once. Possibly because it is called in a loop. ' + 'React Hooks must be called in the exact same order in ' + 'every component render.'
  474. });
  475. } // If this is not a valid code path for React hooks then we need to
  476. // log a warning for every hook in this code path.
  477. //
  478. // Pick a special message depending on the scope this hook was
  479. // called in.
  480. if (isDirectlyInsideComponentOrHook) {
  481. // Report an error if a hook does not reach all finalizing code
  482. // path segments.
  483. //
  484. // Special case when we think there might be an early return.
  485. if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd) {
  486. var message = "React Hook \"" + context.getSource(hook) + "\" is called " + 'conditionally. React Hooks must be called in the exact ' + 'same order in every component render.' + (possiblyHasEarlyReturn ? ' Did you accidentally call a React Hook after an' + ' early return?' : '');
  487. context.report({
  488. node: hook,
  489. message: message
  490. });
  491. }
  492. } else if (codePathNode.parent && (codePathNode.parent.type === 'MethodDefinition' || codePathNode.parent.type === 'ClassProperty') && codePathNode.parent.value === codePathNode) {
  493. // Custom message for hooks inside a class
  494. var _message = "React Hook \"" + context.getSource(hook) + "\" cannot be called " + 'in a class component. React Hooks must be called in a ' + 'React function component or a custom React Hook function.';
  495. context.report({
  496. node: hook,
  497. message: _message
  498. });
  499. } else if (codePathFunctionName) {
  500. // Custom message if we found an invalid function name.
  501. var _message2 = "React Hook \"" + context.getSource(hook) + "\" is called in " + ("function \"" + context.getSource(codePathFunctionName) + "\" ") + 'that is neither a React function component nor a custom ' + 'React Hook function.' + ' React component names must start with an uppercase letter.' + ' React Hook names must start with the word "use".';
  502. context.report({
  503. node: hook,
  504. message: _message2
  505. });
  506. } else if (codePathNode.type === 'Program') {
  507. // These are dangerous if you have inline requires enabled.
  508. var _message3 = "React Hook \"" + context.getSource(hook) + "\" cannot be called " + 'at the top level. React Hooks must be called in a ' + 'React function component or a custom React Hook function.';
  509. context.report({
  510. node: hook,
  511. message: _message3
  512. });
  513. } else {
  514. // Assume in all other cases the user called a hook in some
  515. // random function callback. This should usually be true for
  516. // anonymous function expressions. Hopefully this is clarifying
  517. // enough in the common case that the incorrect message in
  518. // uncommon cases doesn't matter.
  519. if (isSomewhereInsideComponentOrHook) {
  520. var _message4 = "React Hook \"" + context.getSource(hook) + "\" cannot be called " + 'inside a callback. React Hooks must be called in a ' + 'React function component or a custom React Hook function.';
  521. context.report({
  522. node: hook,
  523. message: _message4
  524. });
  525. }
  526. }
  527. }
  528. } catch (err) {
  529. _iterator8.e(err);
  530. } finally {
  531. _iterator8.f();
  532. }
  533. }
  534. } catch (err) {
  535. _iterator7.e(err);
  536. } finally {
  537. _iterator7.f();
  538. }
  539. },
  540. // Missed opportunity...We could visit all `Identifier`s instead of all
  541. // `CallExpression`s and check that _every use_ of a hook name is valid.
  542. // But that gets complicated and enters type-system territory, so we're
  543. // only being strict about hook calls for now.
  544. CallExpression: function (node) {
  545. if (isHook(node.callee)) {
  546. // Add the hook node to a map keyed by the code path segment. We will
  547. // do full code path analysis at the end of our code path.
  548. var reactHooksMap = last(codePathReactHooksMapStack);
  549. var codePathSegment = last(codePathSegmentStack);
  550. var reactHooks = reactHooksMap.get(codePathSegment);
  551. if (!reactHooks) {
  552. reactHooks = [];
  553. reactHooksMap.set(codePathSegment, reactHooks);
  554. }
  555. reactHooks.push(node.callee);
  556. }
  557. }
  558. };
  559. }
  560. };
  561. /**
  562. * Gets the static name of a function AST node. For function declarations it is
  563. * easy. For anonymous function expressions it is much harder. If you search for
  564. * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
  565. * where JS gives anonymous function expressions names. We roughly detect the
  566. * same AST nodes with some exceptions to better fit our use case.
  567. */
  568. function getFunctionName(node) {
  569. if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' && node.id) {
  570. // function useHook() {}
  571. // const whatever = function useHook() {};
  572. //
  573. // Function declaration or function expression names win over any
  574. // assignment statements or other renames.
  575. return node.id;
  576. } else if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
  577. if (node.parent.type === 'VariableDeclarator' && node.parent.init === node) {
  578. // const useHook = () => {};
  579. return node.parent.id;
  580. } else if (node.parent.type === 'AssignmentExpression' && node.parent.right === node && node.parent.operator === '=') {
  581. // useHook = () => {};
  582. return node.parent.left;
  583. } else if (node.parent.type === 'Property' && node.parent.value === node && !node.parent.computed) {
  584. // {useHook: () => {}}
  585. // {useHook() {}}
  586. return node.parent.key; // NOTE: We could also support `ClassProperty` and `MethodDefinition`
  587. // here to be pedantic. However, hooks in a class are an anti-pattern. So
  588. // we don't allow it to error early.
  589. //
  590. // class {useHook = () => {}}
  591. // class {useHook() {}}
  592. } else if (node.parent.type === 'AssignmentPattern' && node.parent.right === node && !node.parent.computed) {
  593. // const {useHook = () => {}} = {};
  594. // ({useHook = () => {}} = {});
  595. //
  596. // Kinda clowny, but we'd said we'd follow spec convention for
  597. // `IsAnonymousFunctionDefinition()` usage.
  598. return node.parent.left;
  599. } else {
  600. return undefined;
  601. }
  602. } else {
  603. return undefined;
  604. }
  605. }
  606. /**
  607. * Convenience function for peeking the last item in a stack.
  608. */
  609. function last(array) {
  610. return array[array.length - 1];
  611. }
  612. /* eslint-disable no-for-of-loops/no-for-of-loops */
  613. var ExhaustiveDeps = {
  614. meta: {
  615. type: 'suggestion',
  616. docs: {
  617. description: 'verifies the list of dependencies for Hooks like useEffect and similar',
  618. recommended: true,
  619. url: 'https://github.com/facebook/react/issues/14920'
  620. },
  621. fixable: 'code',
  622. hasSuggestions: true,
  623. schema: [{
  624. type: 'object',
  625. additionalProperties: false,
  626. enableDangerousAutofixThisMayCauseInfiniteLoops: false,
  627. properties: {
  628. additionalHooks: {
  629. type: 'string'
  630. },
  631. enableDangerousAutofixThisMayCauseInfiniteLoops: {
  632. type: 'boolean'
  633. }
  634. }
  635. }]
  636. },
  637. create: function (context) {
  638. // Parse the `additionalHooks` regex.
  639. var additionalHooks = context.options && context.options[0] && context.options[0].additionalHooks ? new RegExp(context.options[0].additionalHooks) : undefined;
  640. var enableDangerousAutofixThisMayCauseInfiniteLoops = context.options && context.options[0] && context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops || false;
  641. var options = {
  642. additionalHooks: additionalHooks,
  643. enableDangerousAutofixThisMayCauseInfiniteLoops: enableDangerousAutofixThisMayCauseInfiniteLoops
  644. };
  645. function reportProblem(problem) {
  646. if (enableDangerousAutofixThisMayCauseInfiniteLoops) {
  647. // Used to enable legacy behavior. Dangerous.
  648. // Keep this as an option until major IDEs upgrade (including VSCode FB ESLint extension).
  649. if (Array.isArray(problem.suggest) && problem.suggest.length > 0) {
  650. problem.fix = problem.suggest[0].fix;
  651. }
  652. }
  653. context.report(problem);
  654. }
  655. var scopeManager = context.getSourceCode().scopeManager; // Should be shared between visitors.
  656. var setStateCallSites = new WeakMap();
  657. var stateVariables = new WeakSet();
  658. var stableKnownValueCache = new WeakMap();
  659. var functionWithoutCapturedValueCache = new WeakMap();
  660. function memoizeWithWeakMap(fn, map) {
  661. return function (arg) {
  662. if (map.has(arg)) {
  663. // to verify cache hits:
  664. // console.log(arg.name)
  665. return map.get(arg);
  666. }
  667. var result = fn(arg);
  668. map.set(arg, result);
  669. return result;
  670. };
  671. }
  672. /**
  673. * Visitor for both function expressions and arrow function expressions.
  674. */
  675. function visitFunctionWithDependencies(node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect) {
  676. if (isEffect && node.async) {
  677. reportProblem({
  678. node: node,
  679. message: "Effect callbacks are synchronous to prevent race conditions. " + "Put the async function inside:\n\n" + 'useEffect(() => {\n' + ' async function fetchData() {\n' + ' // You can await here\n' + ' const response = await MyAPI.getData(someId);\n' + ' // ...\n' + ' }\n' + ' fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching'
  680. });
  681. } // Get the current scope.
  682. var scope = scopeManager.acquire(node); // Find all our "pure scopes". On every re-render of a component these
  683. // pure scopes may have changes to the variables declared within. So all
  684. // variables used in our reactive hook callback but declared in a pure
  685. // scope need to be listed as dependencies of our reactive hook callback.
  686. //
  687. // According to the rules of React you can't read a mutable value in pure
  688. // scope. We can't enforce this in a lint so we trust that all variables
  689. // declared outside of pure scope are indeed frozen.
  690. var pureScopes = new Set();
  691. var componentScope = null;
  692. {
  693. var currentScope = scope.upper;
  694. while (currentScope) {
  695. pureScopes.add(currentScope);
  696. if (currentScope.type === 'function') {
  697. break;
  698. }
  699. currentScope = currentScope.upper;
  700. } // If there is no parent function scope then there are no pure scopes.
  701. // The ones we've collected so far are incorrect. So don't continue with
  702. // the lint.
  703. if (!currentScope) {
  704. return;
  705. }
  706. componentScope = currentScope;
  707. }
  708. var isArray = Array.isArray; // Next we'll define a few helpers that helps us
  709. // tell if some values don't have to be declared as deps.
  710. // Some are known to be stable based on Hook calls.
  711. // const [state, setState] = useState() / React.useState()
  712. // ^^^ true for this reference
  713. // const [state, dispatch] = useReducer() / React.useReducer()
  714. // ^^^ true for this reference
  715. // const ref = useRef()
  716. // ^^^ true for this reference
  717. // False for everything else.
  718. function isStableKnownHookValue(resolved) {
  719. if (!isArray(resolved.defs)) {
  720. return false;
  721. }
  722. var def = resolved.defs[0];
  723. if (def == null) {
  724. return false;
  725. } // Look for `let stuff = ...`
  726. if (def.node.type !== 'VariableDeclarator') {
  727. return false;
  728. }
  729. var init = def.node.init;
  730. if (init == null) {
  731. return false;
  732. }
  733. while (init.type === 'TSAsExpression') {
  734. init = init.expression;
  735. } // Detect primitive constants
  736. // const foo = 42
  737. var declaration = def.node.parent;
  738. if (declaration == null) {
  739. // This might happen if variable is declared after the callback.
  740. // In that case ESLint won't set up .parent refs.
  741. // So we'll set them up manually.
  742. fastFindReferenceWithParent(componentScope.block, def.node.id);
  743. declaration = def.node.parent;
  744. if (declaration == null) {
  745. return false;
  746. }
  747. }
  748. if (declaration.kind === 'const' && init.type === 'Literal' && (typeof init.value === 'string' || typeof init.value === 'number' || init.value === null)) {
  749. // Definitely stable
  750. return true;
  751. } // Detect known Hook calls
  752. // const [_, setState] = useState()
  753. if (init.type !== 'CallExpression') {
  754. return false;
  755. }
  756. var callee = init.callee; // Step into `= React.something` initializer.
  757. if (callee.type === 'MemberExpression' && callee.object.name === 'React' && callee.property != null && !callee.computed) {
  758. callee = callee.property;
  759. }
  760. if (callee.type !== 'Identifier') {
  761. return false;
  762. }
  763. var id = def.node.id;
  764. var _callee = callee,
  765. name = _callee.name;
  766. if (name === 'useRef' && id.type === 'Identifier') {
  767. // useRef() return value is stable.
  768. return true;
  769. } else if (name === 'useState' || name === 'useReducer') {
  770. // Only consider second value in initializing tuple stable.
  771. if (id.type === 'ArrayPattern' && id.elements.length === 2 && isArray(resolved.identifiers)) {
  772. // Is second tuple value the same reference we're checking?
  773. if (id.elements[1] === resolved.identifiers[0]) {
  774. if (name === 'useState') {
  775. var references = resolved.references;
  776. var writeCount = 0;
  777. for (var i = 0; i < references.length; i++) {
  778. if (references[i].isWrite()) {
  779. writeCount++;
  780. }
  781. if (writeCount > 1) {
  782. return false;
  783. }
  784. setStateCallSites.set(references[i].identifier, id.elements[0]);
  785. }
  786. } // Setter is stable.
  787. return true;
  788. } else if (id.elements[0] === resolved.identifiers[0]) {
  789. if (name === 'useState') {
  790. var _references = resolved.references;
  791. for (var _i = 0; _i < _references.length; _i++) {
  792. stateVariables.add(_references[_i].identifier);
  793. }
  794. } // State variable itself is dynamic.
  795. return false;
  796. }
  797. }
  798. } else if (name === 'useTransition') {
  799. // Only consider second value in initializing tuple stable.
  800. if (id.type === 'ArrayPattern' && id.elements.length === 2 && Array.isArray(resolved.identifiers)) {
  801. // Is second tuple value the same reference we're checking?
  802. if (id.elements[1] === resolved.identifiers[0]) {
  803. // Setter is stable.
  804. return true;
  805. }
  806. }
  807. } // By default assume it's dynamic.
  808. return false;
  809. } // Some are just functions that don't reference anything dynamic.
  810. function isFunctionWithoutCapturedValues(resolved) {
  811. if (!isArray(resolved.defs)) {
  812. return false;
  813. }
  814. var def = resolved.defs[0];
  815. if (def == null) {
  816. return false;
  817. }
  818. if (def.node == null || def.node.id == null) {
  819. return false;
  820. } // Search the direct component subscopes for
  821. // top-level function definitions matching this reference.
  822. var fnNode = def.node;
  823. var childScopes = componentScope.childScopes;
  824. var fnScope = null;
  825. var i;
  826. for (i = 0; i < childScopes.length; i++) {
  827. var childScope = childScopes[i];
  828. var childScopeBlock = childScope.block;
  829. if ( // function handleChange() {}
  830. fnNode.type === 'FunctionDeclaration' && childScopeBlock === fnNode || // const handleChange = () => {}
  831. // const handleChange = function() {}
  832. fnNode.type === 'VariableDeclarator' && childScopeBlock.parent === fnNode) {
  833. // Found it!
  834. fnScope = childScope;
  835. break;
  836. }
  837. }
  838. if (fnScope == null) {
  839. return false;
  840. } // Does this function capture any values
  841. // that are in pure scopes (aka render)?
  842. for (i = 0; i < fnScope.through.length; i++) {
  843. var ref = fnScope.through[i];
  844. if (ref.resolved == null) {
  845. continue;
  846. }
  847. if (pureScopes.has(ref.resolved.scope) && // Stable values are fine though,
  848. // although we won't check functions deeper.
  849. !memoizedIsStableKnownHookValue(ref.resolved)) {
  850. return false;
  851. }
  852. } // If we got here, this function doesn't capture anything
  853. // from render--or everything it captures is known stable.
  854. return true;
  855. } // Remember such values. Avoid re-running extra checks on them.
  856. var memoizedIsStableKnownHookValue = memoizeWithWeakMap(isStableKnownHookValue, stableKnownValueCache);
  857. var memoizedIsFunctionWithoutCapturedValues = memoizeWithWeakMap(isFunctionWithoutCapturedValues, functionWithoutCapturedValueCache); // These are usually mistaken. Collect them.
  858. var currentRefsInEffectCleanup = new Map(); // Is this reference inside a cleanup function for this effect node?
  859. // We can check by traversing scopes upwards from the reference, and checking
  860. // if the last "return () => " we encounter is located directly inside the effect.
  861. function isInsideEffectCleanup(reference) {
  862. var curScope = reference.from;
  863. var isInReturnedFunction = false;
  864. while (curScope.block !== node) {
  865. if (curScope.type === 'function') {
  866. isInReturnedFunction = curScope.block.parent != null && curScope.block.parent.type === 'ReturnStatement';
  867. }
  868. curScope = curScope.upper;
  869. }
  870. return isInReturnedFunction;
  871. } // Get dependencies from all our resolved references in pure scopes.
  872. // Key is dependency string, value is whether it's stable.
  873. var dependencies = new Map();
  874. var optionalChains = new Map();
  875. gatherDependenciesRecursively(scope);
  876. function gatherDependenciesRecursively(currentScope) {
  877. var _iterator = _createForOfIteratorHelper(currentScope.references),
  878. _step;
  879. try {
  880. for (_iterator.s(); !(_step = _iterator.n()).done;) {
  881. var reference = _step.value;
  882. // If this reference is not resolved or it is not declared in a pure
  883. // scope then we don't care about this reference.
  884. if (!reference.resolved) {
  885. continue;
  886. }
  887. if (!pureScopes.has(reference.resolved.scope)) {
  888. continue;
  889. } // Narrow the scope of a dependency if it is, say, a member expression.
  890. // Then normalize the narrowed dependency.
  891. var referenceNode = fastFindReferenceWithParent(node, reference.identifier);
  892. var dependencyNode = getDependency(referenceNode);
  893. var dependency = analyzePropertyChain(dependencyNode, optionalChains); // Accessing ref.current inside effect cleanup is bad.
  894. if ( // We're in an effect...
  895. isEffect && // ... and this look like accessing .current...
  896. dependencyNode.type === 'Identifier' && (dependencyNode.parent.type === 'MemberExpression' || dependencyNode.parent.type === 'OptionalMemberExpression') && !dependencyNode.parent.computed && dependencyNode.parent.property.type === 'Identifier' && dependencyNode.parent.property.name === 'current' && // ...in a cleanup function or below...
  897. isInsideEffectCleanup(reference)) {
  898. currentRefsInEffectCleanup.set(dependency, {
  899. reference: reference,
  900. dependencyNode: dependencyNode
  901. });
  902. }
  903. if (dependencyNode.parent.type === 'TSTypeQuery' || dependencyNode.parent.type === 'TSTypeReference') {
  904. continue;
  905. }
  906. var def = reference.resolved.defs[0];
  907. if (def == null) {
  908. continue;
  909. } // Ignore references to the function itself as it's not defined yet.
  910. if (def.node != null && def.node.init === node.parent) {
  911. continue;
  912. } // Ignore Flow type parameters
  913. if (def.type === 'TypeParameter') {
  914. continue;
  915. } // Add the dependency to a map so we can make sure it is referenced
  916. // again in our dependencies array. Remember whether it's stable.
  917. if (!dependencies.has(dependency)) {
  918. var resolved = reference.resolved;
  919. var isStable = memoizedIsStableKnownHookValue(resolved) || memoizedIsFunctionWithoutCapturedValues(resolved);
  920. dependencies.set(dependency, {
  921. isStable: isStable,
  922. references: [reference]
  923. });
  924. } else {
  925. dependencies.get(dependency).references.push(reference);
  926. }
  927. }
  928. } catch (err) {
  929. _iterator.e(err);
  930. } finally {
  931. _iterator.f();
  932. }
  933. var _iterator2 = _createForOfIteratorHelper(currentScope.childScopes),
  934. _step2;
  935. try {
  936. for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
  937. var childScope = _step2.value;
  938. gatherDependenciesRecursively(childScope);
  939. }
  940. } catch (err) {
  941. _iterator2.e(err);
  942. } finally {
  943. _iterator2.f();
  944. }
  945. } // Warn about accessing .current in cleanup effects.
  946. currentRefsInEffectCleanup.forEach(function (_ref, dependency) {
  947. var reference = _ref.reference,
  948. dependencyNode = _ref.dependencyNode;
  949. var references = reference.resolved.references; // Is React managing this ref or us?
  950. // Let's see if we can find a .current assignment.
  951. var foundCurrentAssignment = false;
  952. for (var i = 0; i < references.length; i++) {
  953. var identifier = references[i].identifier;
  954. var parent = identifier.parent;
  955. if (parent != null && // ref.current
  956. // Note: no need to handle OptionalMemberExpression because it can't be LHS.
  957. parent.type === 'MemberExpression' && !parent.computed && parent.property.type === 'Identifier' && parent.property.name === 'current' && // ref.current = <something>
  958. parent.parent.type === 'AssignmentExpression' && parent.parent.left === parent) {
  959. foundCurrentAssignment = true;
  960. break;
  961. }
  962. } // We only want to warn about React-managed refs.
  963. if (foundCurrentAssignment) {
  964. return;
  965. }
  966. reportProblem({
  967. node: dependencyNode.parent.property,
  968. message: "The ref value '" + dependency + ".current' will likely have " + "changed by the time this effect cleanup function runs. If " + "this ref points to a node rendered by React, copy " + ("'" + dependency + ".current' to a variable inside the effect, and ") + "use that variable in the cleanup function."
  969. });
  970. }); // Warn about assigning to variables in the outer scope.
  971. // Those are usually bugs.
  972. var staleAssignments = new Set();
  973. function reportStaleAssignment(writeExpr, key) {
  974. if (staleAssignments.has(key)) {
  975. return;
  976. }
  977. staleAssignments.add(key);
  978. reportProblem({
  979. node: writeExpr,
  980. message: "Assignments to the '" + key + "' variable from inside React Hook " + (context.getSource(reactiveHook) + " will be lost after each ") + "render. To preserve the value over time, store it in a useRef " + "Hook and keep the mutable value in the '.current' property. " + "Otherwise, you can move this variable directly inside " + (context.getSource(reactiveHook) + ".")
  981. });
  982. } // Remember which deps are stable and report bad usage first.
  983. var stableDependencies = new Set();
  984. dependencies.forEach(function (_ref2, key) {
  985. var isStable = _ref2.isStable,
  986. references = _ref2.references;
  987. if (isStable) {
  988. stableDependencies.add(key);
  989. }
  990. references.forEach(function (reference) {
  991. if (reference.writeExpr) {
  992. reportStaleAssignment(reference.writeExpr, key);
  993. }
  994. });
  995. });
  996. if (staleAssignments.size > 0) {
  997. // The intent isn't clear so we'll wait until you fix those first.
  998. return;
  999. }
  1000. if (!declaredDependenciesNode) {
  1001. // Check if there are any top-level setState() calls.
  1002. // Those tend to lead to infinite loops.
  1003. var setStateInsideEffectWithoutDeps = null;
  1004. dependencies.forEach(function (_ref3, key) {
  1005. var isStable = _ref3.isStable,
  1006. references = _ref3.references;
  1007. if (setStateInsideEffectWithoutDeps) {
  1008. return;
  1009. }
  1010. references.forEach(function (reference) {
  1011. if (setStateInsideEffectWithoutDeps) {
  1012. return;
  1013. }
  1014. var id = reference.identifier;
  1015. var isSetState = setStateCallSites.has(id);
  1016. if (!isSetState) {
  1017. return;
  1018. }
  1019. var fnScope = reference.from;
  1020. while (fnScope.type !== 'function') {
  1021. fnScope = fnScope.upper;
  1022. }
  1023. var isDirectlyInsideEffect = fnScope.block === node;
  1024. if (isDirectlyInsideEffect) {
  1025. // TODO: we could potentially ignore early returns.
  1026. setStateInsideEffectWithoutDeps = key;
  1027. }
  1028. });
  1029. });
  1030. if (setStateInsideEffectWithoutDeps) {
  1031. var _collectRecommendatio = collectRecommendations({
  1032. dependencies: dependencies,
  1033. declaredDependencies: [],
  1034. stableDependencies: stableDependencies,
  1035. externalDependencies: new Set(),
  1036. isEffect: true
  1037. }),
  1038. _suggestedDependencies = _collectRecommendatio.suggestedDependencies;
  1039. reportProblem({
  1040. node: reactiveHook,
  1041. message: "React Hook " + reactiveHookName + " contains a call to '" + setStateInsideEffectWithoutDeps + "'. " + "Without a list of dependencies, this can lead to an infinite chain of updates. " + "To fix this, pass [" + _suggestedDependencies.join(', ') + ("] as a second argument to the " + reactiveHookName + " Hook."),
  1042. suggest: [{
  1043. desc: "Add dependencies array: [" + _suggestedDependencies.join(', ') + "]",
  1044. fix: function (fixer) {
  1045. return fixer.insertTextAfter(node, ", [" + _suggestedDependencies.join(', ') + "]");
  1046. }
  1047. }]
  1048. });
  1049. }
  1050. return;
  1051. }
  1052. var declaredDependencies = [];
  1053. var externalDependencies = new Set();
  1054. if (declaredDependenciesNode.type !== 'ArrayExpression') {
  1055. // If the declared dependencies are not an array expression then we
  1056. // can't verify that the user provided the correct dependencies. Tell
  1057. // the user this in an error.
  1058. reportProblem({
  1059. node: declaredDependenciesNode,
  1060. message: "React Hook " + context.getSource(reactiveHook) + " was passed a " + 'dependency list that is not an array literal. This means we ' + "can't statically verify whether you've passed the correct " + 'dependencies.'
  1061. });
  1062. } else {
  1063. declaredDependenciesNode.elements.forEach(function (declaredDependencyNode) {
  1064. // Skip elided elements.
  1065. if (declaredDependencyNode === null) {
  1066. return;
  1067. } // If we see a spread element then add a special warning.
  1068. if (declaredDependencyNode.type === 'SpreadElement') {
  1069. reportProblem({
  1070. node: declaredDependencyNode,
  1071. message: "React Hook " + context.getSource(reactiveHook) + " has a spread " + "element in its dependency array. This means we can't " + "statically verify whether you've passed the " + 'correct dependencies.'
  1072. });
  1073. return;
  1074. } // Try to normalize the declared dependency. If we can't then an error
  1075. // will be thrown. We will catch that error and report an error.
  1076. var declaredDependency;
  1077. try {
  1078. declaredDependency = analyzePropertyChain(declaredDependencyNode, null);
  1079. } catch (error) {
  1080. if (/Unsupported node type/.test(error.message)) {
  1081. if (declaredDependencyNode.type === 'Literal') {
  1082. if (dependencies.has(declaredDependencyNode.value)) {
  1083. reportProblem({
  1084. node: declaredDependencyNode,
  1085. message: "The " + declaredDependencyNode.raw + " literal is not a valid dependency " + "because it never changes. " + ("Did you mean to include " + declaredDependencyNode.value + " in the array instead?")
  1086. });
  1087. } else {
  1088. reportProblem({
  1089. node: declaredDependencyNode,
  1090. message: "The " + declaredDependencyNode.raw + " literal is not a valid dependency " + 'because it never changes. You can safely remove it.'
  1091. });
  1092. }
  1093. } else {
  1094. reportProblem({
  1095. node: declaredDependencyNode,
  1096. message: "React Hook " + context.getSource(reactiveHook) + " has a " + "complex expression in the dependency array. " + 'Extract it to a separate variable so it can be statically checked.'
  1097. });
  1098. }
  1099. return;
  1100. } else {
  1101. throw error;
  1102. }
  1103. }
  1104. var maybeID = declaredDependencyNode;
  1105. while (maybeID.type === 'MemberExpression' || maybeID.type === 'OptionalMemberExpression' || maybeID.type === 'ChainExpression') {
  1106. maybeID = maybeID.object || maybeID.expression.object;
  1107. }
  1108. var isDeclaredInComponent = !componentScope.through.some(function (ref) {
  1109. return ref.identifier === maybeID;
  1110. }); // Add the dependency to our declared dependency map.
  1111. declaredDependencies.push({
  1112. key: declaredDependency,
  1113. node: declaredDependencyNode
  1114. });
  1115. if (!isDeclaredInComponent) {
  1116. externalDependencies.add(declaredDependency);
  1117. }
  1118. });
  1119. }
  1120. var _collectRecommendatio2 = collectRecommendations({
  1121. dependencies: dependencies,
  1122. declaredDependencies: declaredDependencies,
  1123. stableDependencies: stableDependencies,
  1124. externalDependencies: externalDependencies,
  1125. isEffect: isEffect
  1126. }),
  1127. suggestedDependencies = _collectRecommendatio2.suggestedDependencies,
  1128. unnecessaryDependencies = _collectRecommendatio2.unnecessaryDependencies,
  1129. missingDependencies = _collectRecommendatio2.missingDependencies,
  1130. duplicateDependencies = _collectRecommendatio2.duplicateDependencies;
  1131. var suggestedDeps = suggestedDependencies;
  1132. var problemCount = duplicateDependencies.size + missingDependencies.size + unnecessaryDependencies.size;
  1133. if (problemCount === 0) {
  1134. // If nothing else to report, check if some dependencies would
  1135. // invalidate on every render.
  1136. var constructions = scanForConstructions({
  1137. declaredDependencies: declaredDependencies,
  1138. declaredDependenciesNode: declaredDependenciesNode,
  1139. componentScope: componentScope,
  1140. scope: scope
  1141. });
  1142. constructions.forEach(function (_ref4) {
  1143. var construction = _ref4.construction,
  1144. isUsedOutsideOfHook = _ref4.isUsedOutsideOfHook,
  1145. depType = _ref4.depType;
  1146. var wrapperHook = depType === 'function' ? 'useCallback' : 'useMemo';
  1147. var constructionType = depType === 'function' ? 'definition' : 'initialization';
  1148. var defaultAdvice = "wrap the " + constructionType + " of '" + construction.name.name + "' in its own " + wrapperHook + "() Hook.";
  1149. var advice = isUsedOutsideOfHook ? "To fix this, " + defaultAdvice : "Move it inside the " + reactiveHookName + " callback. Alternatively, " + defaultAdvice;
  1150. var causation = depType === 'conditional' || depType === 'logical expression' ? 'could make' : 'makes';
  1151. var message = "The '" + construction.name.name + "' " + depType + " " + causation + " the dependencies of " + (reactiveHookName + " Hook (at line " + declaredDependenciesNode.loc.start.line + ") ") + ("change on every render. " + advice);
  1152. var suggest; // Only handle the simple case of variable assignments.
  1153. // Wrapping function declarations can mess up hoisting.
  1154. if (isUsedOutsideOfHook && construction.type === 'Variable' && // Objects may be mutated after construction, which would make this
  1155. // fix unsafe. Functions _probably_ won't be mutated, so we'll
  1156. // allow this fix for them.
  1157. depType === 'function') {
  1158. suggest = [{
  1159. desc: "Wrap the " + constructionType + " of '" + construction.name.name + "' in its own " + wrapperHook + "() Hook.",
  1160. fix: function (fixer) {
  1161. var _ref5 = wrapperHook === 'useMemo' ? ["useMemo(() => { return ", '; })'] : ['useCallback(', ')'],
  1162. before = _ref5[0],
  1163. after = _ref5[1];
  1164. return [// TODO: also add an import?
  1165. fixer.insertTextBefore(construction.node.init, before), // TODO: ideally we'd gather deps here but it would require
  1166. // restructuring the rule code. This will cause a new lint
  1167. // error to appear immediately for useCallback. Note we're
  1168. // not adding [] because would that changes semantics.
  1169. fixer.insertTextAfter(construction.node.init, after)];
  1170. }
  1171. }];
  1172. } // TODO: What if the function needs to change on every render anyway?
  1173. // Should we suggest removing effect deps as an appropriate fix too?
  1174. reportProblem({
  1175. // TODO: Why not report this at the dependency site?
  1176. node: construction.node,
  1177. message: message,
  1178. suggest: suggest
  1179. });
  1180. });
  1181. return;
  1182. } // If we're going to report a missing dependency,
  1183. // we might as well recalculate the list ignoring
  1184. // the currently specified deps. This can result
  1185. // in some extra deduplication. We can't do this
  1186. // for effects though because those have legit
  1187. // use cases for over-specifying deps.
  1188. if (!isEffect && missingDependencies.size > 0) {
  1189. suggestedDeps = collectRecommendations({
  1190. dependencies: dependencies,
  1191. declaredDependencies: [],
  1192. // Pretend we don't know
  1193. stableDependencies: stableDependencies,
  1194. externalDependencies: externalDependencies,
  1195. isEffect: isEffect
  1196. }).suggestedDependencies;
  1197. } // Alphabetize the suggestions, but only if deps were already alphabetized.
  1198. function areDeclaredDepsAlphabetized() {
  1199. if (declaredDependencies.length === 0) {
  1200. return true;
  1201. }
  1202. var declaredDepKeys = declaredDependencies.map(function (dep) {
  1203. return dep.key;
  1204. });
  1205. var sortedDeclaredDepKeys = declaredDepKeys.slice().sort();
  1206. return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(',');
  1207. }
  1208. if (areDeclaredDepsAlphabetized()) {
  1209. suggestedDeps.sort();
  1210. } // Most of our algorithm deals with dependency paths with optional chaining stripped.
  1211. // This function is the last step before printing a dependency, so now is a good time to
  1212. // check whether any members in our path are always used as optional-only. In that case,
  1213. // we will use ?. instead of . to concatenate those parts of the path.
  1214. function formatDependency(path) {
  1215. var members = path.split('.');
  1216. var finalPath = '';
  1217. for (var i = 0; i < members.length; i++) {
  1218. if (i !== 0) {
  1219. var pathSoFar = members.slice(0, i + 1).join('.');
  1220. var isOptional = optionalChains.get(pathSoFar) === true;
  1221. finalPath += isOptional ? '?.' : '.';
  1222. }
  1223. finalPath += members[i];
  1224. }
  1225. return finalPath;
  1226. }
  1227. function getWarningMessage(deps, singlePrefix, label, fixVerb) {
  1228. if (deps.size === 0) {
  1229. return null;
  1230. }
  1231. return (deps.size > 1 ? '' : singlePrefix + ' ') + label + ' ' + (deps.size > 1 ? 'dependencies' : 'dependency') + ': ' + joinEnglish(Array.from(deps).sort().map(function (name) {
  1232. return "'" + formatDependency(name) + "'";
  1233. })) + (". Either " + fixVerb + " " + (deps.size > 1 ? 'them' : 'it') + " or remove the dependency array.");
  1234. }
  1235. var extraWarning = '';
  1236. if (unnecessaryDependencies.size > 0) {
  1237. var badRef = null;
  1238. Array.from(unnecessaryDependencies.keys()).forEach(function (key) {
  1239. if (badRef !== null) {
  1240. return;
  1241. }
  1242. if (key.endsWith('.current')) {
  1243. badRef = key;
  1244. }
  1245. });
  1246. if (badRef !== null) {
  1247. extraWarning = " Mutable values like '" + badRef + "' aren't valid dependencies " + "because mutating them doesn't re-render the component.";
  1248. } else if (externalDependencies.size > 0) {
  1249. var dep = Array.from(externalDependencies)[0]; // Don't show this warning for things that likely just got moved *inside* the callback
  1250. // because in that case they're clearly not referring to globals.
  1251. if (!scope.set.has(dep)) {
  1252. extraWarning = " Outer scope values like '" + dep + "' aren't valid dependencies " + "because mutating them doesn't re-render the component.";
  1253. }
  1254. }
  1255. } // `props.foo()` marks `props` as a dependency because it has
  1256. // a `this` value. This warning can be confusing.
  1257. // So if we're going to show it, append a clarification.
  1258. if (!extraWarning && missingDependencies.has('props')) {
  1259. var propDep = dependencies.get('props');
  1260. if (propDep == null) {
  1261. return;
  1262. }
  1263. var refs = propDep.references;
  1264. if (!Array.isArray(refs)) {
  1265. return;
  1266. }
  1267. var isPropsOnlyUsedInMembers = true;
  1268. for (var i = 0; i < refs.length; i++) {
  1269. var ref = refs[i];
  1270. var id = fastFindReferenceWithParent(componentScope.block, ref.identifier);
  1271. if (!id) {
  1272. isPropsOnlyUsedInMembers = false;
  1273. break;
  1274. }
  1275. var parent = id.parent;
  1276. if (parent == null) {
  1277. isPropsOnlyUsedInMembers = false;
  1278. break;
  1279. }
  1280. if (parent.type !== 'MemberExpression' && parent.type !== 'OptionalMemberExpression') {
  1281. isPropsOnlyUsedInMembers = false;
  1282. break;
  1283. }
  1284. }
  1285. if (isPropsOnlyUsedInMembers) {
  1286. extraWarning = " However, 'props' will change when *any* prop changes, so the " + "preferred fix is to destructure the 'props' object outside of " + ("the " + reactiveHookName + " call and refer to those specific props ") + ("inside " + context.getSource(reactiveHook) + ".");
  1287. }
  1288. }
  1289. if (!extraWarning && missingDependencies.size > 0) {
  1290. // See if the user is trying to avoid specifying a callable prop.
  1291. // This usually means they're unaware of useCallback.
  1292. var missingCallbackDep = null;
  1293. missingDependencies.forEach(function (missingDep) {
  1294. if (missingCallbackDep) {
  1295. return;
  1296. } // Is this a variable from top scope?
  1297. var topScopeRef = componentScope.set.get(missingDep);
  1298. var usedDep = dependencies.get(missingDep);
  1299. if (usedDep.references[0].resolved !== topScopeRef) {
  1300. return;
  1301. } // Is this a destructured prop?
  1302. var def = topScopeRef.defs[0];
  1303. if (def == null || def.name == null || def.type !== 'Parameter') {
  1304. return;
  1305. } // Was it called in at least one case? Then it's a function.
  1306. var isFunctionCall = false;
  1307. var id;
  1308. for (var _i2 = 0; _i2 < usedDep.references.length; _i2++) {
  1309. id = usedDep.references[_i2].identifier;
  1310. if (id != null && id.parent != null && (id.parent.type === 'CallExpression' || id.parent.type === 'OptionalCallExpression') && id.parent.callee === id) {
  1311. isFunctionCall = true;
  1312. break;
  1313. }
  1314. }
  1315. if (!isFunctionCall) {
  1316. return;
  1317. } // If it's missing (i.e. in component scope) *and* it's a parameter
  1318. // then it is definitely coming from props destructuring.
  1319. // (It could also be props itself but we wouldn't be calling it then.)
  1320. missingCallbackDep = missingDep;
  1321. });
  1322. if (missingCallbackDep !== null) {
  1323. extraWarning = " If '" + missingCallbackDep + "' changes too often, " + "find the parent component that defines it " + "and wrap that definition in useCallback.";
  1324. }
  1325. }
  1326. if (!extraWarning && missingDependencies.size > 0) {
  1327. var setStateRecommendation = null;
  1328. missingDependencies.forEach(function (missingDep) {
  1329. if (setStateRecommendation !== null) {
  1330. return;
  1331. }
  1332. var usedDep = dependencies.get(missingDep);
  1333. var references = usedDep.references;
  1334. var id;
  1335. var maybeCall;
  1336. for (var _i3 = 0; _i3 < references.length; _i3++) {
  1337. id = references[_i3].identifier;
  1338. maybeCall = id.parent; // Try to see if we have setState(someExpr(missingDep)).
  1339. while (maybeCall != null && maybeCall !== componentScope.block) {
  1340. if (maybeCall.type === 'CallExpression') {
  1341. var correspondingStateVariable = setStateCallSites.get(maybeCall.callee);
  1342. if (correspondingStateVariable != null) {
  1343. if (correspondingStateVariable.name === missingDep) {
  1344. // setCount(count + 1)
  1345. setStateRecommendation = {
  1346. missingDep: missingDep,
  1347. setter: maybeCall.callee.name,
  1348. form: 'updater'
  1349. };
  1350. } else if (stateVariables.has(id)) {
  1351. // setCount(count + increment)
  1352. setStateRecommendation = {
  1353. missingDep: missingDep,
  1354. setter: maybeCall.callee.name,
  1355. form: 'reducer'
  1356. };
  1357. } else {
  1358. var resolved = references[_i3].resolved;
  1359. if (resolved != null) {
  1360. // If it's a parameter *and* a missing dep,
  1361. // it must be a prop or something inside a prop.
  1362. // Therefore, recommend an inline reducer.
  1363. var def = resolved.defs[0];
  1364. if (def != null && def.type === 'Parameter') {
  1365. setStateRecommendation = {
  1366. missingDep: missingDep,
  1367. setter: maybeCall.callee.name,
  1368. form: 'inlineReducer'
  1369. };
  1370. }
  1371. }
  1372. }
  1373. break;
  1374. }
  1375. }
  1376. maybeCall = maybeCall.parent;
  1377. }
  1378. if (setStateRecommendation !== null) {
  1379. break;
  1380. }
  1381. }
  1382. });
  1383. if (setStateRecommendation !== null) {
  1384. switch (setStateRecommendation.form) {
  1385. case 'reducer':
  1386. extraWarning = " You can also replace multiple useState variables with useReducer " + ("if '" + setStateRecommendation.setter + "' needs the ") + ("current value of '" + setStateRecommendation.missingDep + "'.");
  1387. break;
  1388. case 'inlineReducer':
  1389. extraWarning = " If '" + setStateRecommendation.setter + "' needs the " + ("current value of '" + setStateRecommendation.missingDep + "', ") + "you can also switch to useReducer instead of useState and " + ("read '" + setStateRecommendation.missingDep + "' in the reducer.");
  1390. break;
  1391. case 'updater':
  1392. extraWarning = " You can also do a functional update '" + setStateRecommendation.setter + "(" + setStateRecommendation.missingDep.substring(0, 1) + " => ...)' if you only need '" + setStateRecommendation.missingDep + "'" + (" in the '" + setStateRecommendation.setter + "' call.");
  1393. break;
  1394. default:
  1395. throw new Error('Unknown case.');
  1396. }
  1397. }
  1398. }
  1399. reportProblem({
  1400. node: declaredDependenciesNode,
  1401. message: "React Hook " + context.getSource(reactiveHook) + " has " + ( // To avoid a long message, show the next actionable item.
  1402. getWarningMessage(missingDependencies, 'a', 'missing', 'include') || getWarningMessage(unnecessaryDependencies, 'an', 'unnecessary', 'exclude') || getWarningMessage(duplicateDependencies, 'a', 'duplicate', 'omit')) + extraWarning,
  1403. suggest: [{
  1404. desc: "Update the dependencies array to be: [" + suggestedDeps.map(formatDependency).join(', ') + "]",
  1405. fix: function (fixer) {
  1406. // TODO: consider preserving the comments or formatting?
  1407. return fixer.replaceText(declaredDependenciesNode, "[" + suggestedDeps.map(formatDependency).join(', ') + "]");
  1408. }
  1409. }]
  1410. });
  1411. }
  1412. function visitCallExpression(node) {
  1413. var callbackIndex = getReactiveHookCallbackIndex(node.callee, options);
  1414. if (callbackIndex === -1) {
  1415. // Not a React Hook call that needs deps.
  1416. return;
  1417. }
  1418. var callback = node.arguments[callbackIndex];
  1419. var reactiveHook = node.callee;
  1420. var reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
  1421. var declaredDependenciesNode = node.arguments[callbackIndex + 1];
  1422. var isEffect = /Effect($|[^a-z])/g.test(reactiveHookName); // Check whether a callback is supplied. If there is no callback supplied
  1423. // then the hook will not work and React will throw a TypeError.
  1424. // So no need to check for dependency inclusion.
  1425. if (!callback) {
  1426. reportProblem({
  1427. node: reactiveHook,
  1428. message: "React Hook " + reactiveHookName + " requires an effect callback. " + "Did you forget to pass a callback to the hook?"
  1429. });
  1430. return;
  1431. } // Check the declared dependencies for this reactive hook. If there is no
  1432. // second argument then the reactive callback will re-run on every render.
  1433. // So no need to check for dependency inclusion.
  1434. if (!declaredDependenciesNode && !isEffect) {
  1435. // These are only used for optimization.
  1436. if (reactiveHookName === 'useMemo' || reactiveHookName === 'useCallback') {
  1437. // TODO: Can this have a suggestion?
  1438. reportProblem({
  1439. node: reactiveHook,
  1440. message: "React Hook " + reactiveHookName + " does nothing when called with " + "only one argument. Did you forget to pass an array of " + "dependencies?"
  1441. });
  1442. }
  1443. return;
  1444. }
  1445. switch (callback.type) {
  1446. case 'FunctionExpression':
  1447. case 'ArrowFunctionExpression':
  1448. visitFunctionWithDependencies(callback, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect);
  1449. return;
  1450. // Handled
  1451. case 'Identifier':
  1452. if (!declaredDependenciesNode) {
  1453. // No deps, no problems.
  1454. return; // Handled
  1455. } // The function passed as a callback is not written inline.
  1456. // But perhaps it's in the dependencies array?
  1457. if (declaredDependenciesNode.elements && declaredDependenciesNode.elements.some(function (el) {
  1458. return el && el.type === 'Identifier' && el.name === callback.name;
  1459. })) {
  1460. // If it's already in the list of deps, we don't care because
  1461. // this is valid regardless.
  1462. return; // Handled
  1463. } // We'll do our best effort to find it, complain otherwise.
  1464. var variable = context.getScope().set.get(callback.name);
  1465. if (variable == null || variable.defs == null) {
  1466. // If it's not in scope, we don't care.
  1467. return; // Handled
  1468. } // The function passed as a callback is not written inline.
  1469. // But it's defined somewhere in the render scope.
  1470. // We'll do our best effort to find and check it, complain otherwise.
  1471. var def = variable.defs[0];
  1472. if (!def || !def.node) {
  1473. break; // Unhandled
  1474. }
  1475. if (def.type !== 'Variable' && def.type !== 'FunctionName') {
  1476. // Parameter or an unusual pattern. Bail out.
  1477. break; // Unhandled
  1478. }
  1479. switch (def.node.type) {
  1480. case 'FunctionDeclaration':
  1481. // useEffect(() => { ... }, []);
  1482. visitFunctionWithDependencies(def.node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect);
  1483. return;
  1484. // Handled
  1485. case 'VariableDeclarator':
  1486. var init = def.node.init;
  1487. if (!init) {
  1488. break; // Unhandled
  1489. }
  1490. switch (init.type) {
  1491. // const effectBody = () => {...};
  1492. // useEffect(effectBody, []);
  1493. case 'ArrowFunctionExpression':
  1494. case 'FunctionExpression':
  1495. // We can inspect this function as if it were inline.
  1496. visitFunctionWithDependencies(init, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect);
  1497. return;
  1498. // Handled
  1499. }
  1500. break;
  1501. // Unhandled
  1502. }
  1503. break;
  1504. // Unhandled
  1505. default:
  1506. // useEffect(generateEffectBody(), []);
  1507. reportProblem({
  1508. node: reactiveHook,
  1509. message: "React Hook " + reactiveHookName + " received a function whose dependencies " + "are unknown. Pass an inline function instead."
  1510. });
  1511. return;
  1512. // Handled
  1513. } // Something unusual. Fall back to suggesting to add the body itself as a dep.
  1514. reportProblem({
  1515. node: reactiveHook,
  1516. message: "React Hook " + reactiveHookName + " has a missing dependency: '" + callback.name + "'. " + "Either include it or remove the dependency array.",
  1517. suggest: [{
  1518. desc: "Update the dependencies array to be: [" + callback.name + "]",
  1519. fix: function (fixer) {
  1520. return fixer.replaceText(declaredDependenciesNode, "[" + callback.name + "]");
  1521. }
  1522. }]
  1523. });
  1524. }
  1525. return {
  1526. CallExpression: visitCallExpression
  1527. };
  1528. }
  1529. }; // The meat of the logic.
  1530. function collectRecommendations(_ref6) {
  1531. var dependencies = _ref6.dependencies,
  1532. declaredDependencies = _ref6.declaredDependencies,
  1533. stableDependencies = _ref6.stableDependencies,
  1534. externalDependencies = _ref6.externalDependencies,
  1535. isEffect = _ref6.isEffect;
  1536. // Our primary data structure.
  1537. // It is a logical representation of property chains:
  1538. // `props` -> `props.foo` -> `props.foo.bar` -> `props.foo.bar.baz`
  1539. // -> `props.lol`
  1540. // -> `props.huh` -> `props.huh.okay`
  1541. // -> `props.wow`
  1542. // We'll use it to mark nodes that are *used* by the programmer,
  1543. // and the nodes that were *declared* as deps. Then we will
  1544. // traverse it to learn which deps are missing or unnecessary.
  1545. var depTree = createDepTree();
  1546. function createDepTree() {
  1547. return {
  1548. isUsed: false,
  1549. // True if used in code
  1550. isSatisfiedRecursively: false,
  1551. // True if specified in deps
  1552. isSubtreeUsed: false,
  1553. // True if something deeper is used by code
  1554. children: new Map() // Nodes for properties
  1555. };
  1556. } // Mark all required nodes first.
  1557. // Imagine exclamation marks next to each used deep property.
  1558. dependencies.forEach(function (_, key) {
  1559. var node = getOrCreateNodeByPath(depTree, key);
  1560. node.isUsed = true;
  1561. markAllParentsByPath(depTree, key, function (parent) {
  1562. parent.isSubtreeUsed = true;
  1563. });
  1564. }); // Mark all satisfied nodes.
  1565. // Imagine checkmarks next to each declared dependency.
  1566. declaredDependencies.forEach(function (_ref7) {
  1567. var key = _ref7.key;
  1568. var node = getOrCreateNodeByPath(depTree, key);
  1569. node.isSatisfiedRecursively = true;
  1570. });
  1571. stableDependencies.forEach(function (key) {
  1572. var node = getOrCreateNodeByPath(depTree, key);
  1573. node.isSatisfiedRecursively = true;
  1574. }); // Tree manipulation helpers.
  1575. function getOrCreateNodeByPath(rootNode, path) {
  1576. var keys = path.split('.');
  1577. var node = rootNode;
  1578. var _iterator3 = _createForOfIteratorHelper(keys),
  1579. _step3;
  1580. try {
  1581. for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
  1582. var key = _step3.value;
  1583. var child = node.children.get(key);
  1584. if (!child) {
  1585. child = createDepTree();
  1586. node.children.set(key, child);
  1587. }
  1588. node = child;
  1589. }
  1590. } catch (err) {
  1591. _iterator3.e(err);
  1592. } finally {
  1593. _iterator3.f();
  1594. }
  1595. return node;
  1596. }
  1597. function markAllParentsByPath(rootNode, path, fn) {
  1598. var keys = path.split('.');
  1599. var node = rootNode;
  1600. var _iterator4 = _createForOfIteratorHelper(keys),
  1601. _step4;
  1602. try {
  1603. for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
  1604. var key = _step4.value;
  1605. var child = node.children.get(key);
  1606. if (!child) {
  1607. return;
  1608. }
  1609. fn(child);
  1610. node = child;
  1611. }
  1612. } catch (err) {
  1613. _iterator4.e(err);
  1614. } finally {
  1615. _iterator4.f();
  1616. }
  1617. } // Now we can learn which dependencies are missing or necessary.
  1618. var missingDependencies = new Set();
  1619. var satisfyingDependencies = new Set();
  1620. scanTreeRecursively(depTree, missingDependencies, satisfyingDependencies, function (key) {
  1621. return key;
  1622. });
  1623. function scanTreeRecursively(node, missingPaths, satisfyingPaths, keyToPath) {
  1624. node.children.forEach(function (child, key) {
  1625. var path = keyToPath(key);
  1626. if (child.isSatisfiedRecursively) {
  1627. if (child.isSubtreeUsed) {
  1628. // Remember this dep actually satisfied something.
  1629. satisfyingPaths.add(path);
  1630. } // It doesn't matter if there's something deeper.
  1631. // It would be transitively satisfied since we assume immutability.
  1632. // `props.foo` is enough if you read `props.foo.id`.
  1633. return;
  1634. }
  1635. if (child.isUsed) {
  1636. // Remember that no declared deps satisfied this node.
  1637. missingPaths.add(path); // If we got here, nothing in its subtree was satisfied.
  1638. // No need to search further.
  1639. return;
  1640. }
  1641. scanTreeRecursively(child, missingPaths, satisfyingPaths, function (childKey) {
  1642. return path + '.' + childKey;
  1643. });
  1644. });
  1645. } // Collect suggestions in the order they were originally specified.
  1646. var suggestedDependencies = [];
  1647. var unnecessaryDependencies = new Set();
  1648. var duplicateDependencies = new Set();
  1649. declaredDependencies.forEach(function (_ref8) {
  1650. var key = _ref8.key;
  1651. // Does this declared dep satisfy a real need?
  1652. if (satisfyingDependencies.has(key)) {
  1653. if (suggestedDependencies.indexOf(key) === -1) {
  1654. // Good one.
  1655. suggestedDependencies.push(key);
  1656. } else {
  1657. // Duplicate.
  1658. duplicateDependencies.add(key);
  1659. }
  1660. } else {
  1661. if (isEffect && !key.endsWith('.current') && !externalDependencies.has(key)) {
  1662. // Effects are allowed extra "unnecessary" deps.
  1663. // Such as resetting scroll when ID changes.
  1664. // Consider them legit.
  1665. // The exception is ref.current which is always wrong.
  1666. if (suggestedDependencies.indexOf(key) === -1) {
  1667. suggestedDependencies.push(key);
  1668. }
  1669. } else {
  1670. // It's definitely not needed.
  1671. unnecessaryDependencies.add(key);
  1672. }
  1673. }
  1674. }); // Then add the missing ones at the end.
  1675. missingDependencies.forEach(function (key) {
  1676. suggestedDependencies.push(key);
  1677. });
  1678. return {
  1679. suggestedDependencies: suggestedDependencies,
  1680. unnecessaryDependencies: unnecessaryDependencies,
  1681. duplicateDependencies: duplicateDependencies,
  1682. missingDependencies: missingDependencies
  1683. };
  1684. } // If the node will result in constructing a referentially unique value, return
  1685. // its human readable type name, else return null.
  1686. function getConstructionExpressionType(node) {
  1687. switch (node.type) {
  1688. case 'ObjectExpression':
  1689. return 'object';
  1690. case 'ArrayExpression':
  1691. return 'array';
  1692. case 'ArrowFunctionExpression':
  1693. case 'FunctionExpression':
  1694. return 'function';
  1695. case 'ClassExpression':
  1696. return 'class';
  1697. case 'ConditionalExpression':
  1698. if (getConstructionExpressionType(node.consequent) != null || getConstructionExpressionType(node.alternate) != null) {
  1699. return 'conditional';
  1700. }
  1701. return null;
  1702. case 'LogicalExpression':
  1703. if (getConstructionExpressionType(node.left) != null || getConstructionExpressionType(node.right) != null) {
  1704. return 'logical expression';
  1705. }
  1706. return null;
  1707. case 'JSXFragment':
  1708. return 'JSX fragment';
  1709. case 'JSXElement':
  1710. return 'JSX element';
  1711. case 'AssignmentExpression':
  1712. if (getConstructionExpressionType(node.right) != null) {
  1713. return 'assignment expression';
  1714. }
  1715. return null;
  1716. case 'NewExpression':
  1717. return 'object construction';
  1718. case 'Literal':
  1719. if (node.value instanceof RegExp) {
  1720. return 'regular expression';
  1721. }
  1722. return null;
  1723. case 'TypeCastExpression':
  1724. return getConstructionExpressionType(node.expression);
  1725. case 'TSAsExpression':
  1726. return getConstructionExpressionType(node.expression);
  1727. }
  1728. return null;
  1729. } // Finds variables declared as dependencies
  1730. // that would invalidate on every render.
  1731. function scanForConstructions(_ref9) {
  1732. var declaredDependencies = _ref9.declaredDependencies,
  1733. declaredDependenciesNode = _ref9.declaredDependenciesNode,
  1734. componentScope = _ref9.componentScope,
  1735. scope = _ref9.scope;
  1736. var constructions = declaredDependencies.map(function (_ref10) {
  1737. var key = _ref10.key;
  1738. var ref = componentScope.variables.find(function (v) {
  1739. return v.name === key;
  1740. });
  1741. if (ref == null) {
  1742. return null;
  1743. }
  1744. var node = ref.defs[0];
  1745. if (node == null) {
  1746. return null;
  1747. } // const handleChange = function () {}
  1748. // const handleChange = () => {}
  1749. // const foo = {}
  1750. // const foo = []
  1751. // etc.
  1752. if (node.type === 'Variable' && node.node.type === 'VariableDeclarator' && node.node.id.type === 'Identifier' && // Ensure this is not destructed assignment
  1753. node.node.init != null) {
  1754. var constantExpressionType = getConstructionExpressionType(node.node.init);
  1755. if (constantExpressionType != null) {
  1756. return [ref, constantExpressionType];
  1757. }
  1758. } // function handleChange() {}
  1759. if (node.type === 'FunctionName' && node.node.type === 'FunctionDeclaration') {
  1760. return [ref, 'function'];
  1761. } // class Foo {}
  1762. if (node.type === 'ClassName' && node.node.type === 'ClassDeclaration') {
  1763. return [ref, 'class'];
  1764. }
  1765. return null;
  1766. }).filter(Boolean);
  1767. function isUsedOutsideOfHook(ref) {
  1768. var foundWriteExpr = false;
  1769. for (var i = 0; i < ref.references.length; i++) {
  1770. var reference = ref.references[i];
  1771. if (reference.writeExpr) {
  1772. if (foundWriteExpr) {
  1773. // Two writes to the same function.
  1774. return true;
  1775. } else {
  1776. // Ignore first write as it's not usage.
  1777. foundWriteExpr = true;
  1778. continue;
  1779. }
  1780. }
  1781. var currentScope = reference.from;
  1782. while (currentScope !== scope && currentScope != null) {
  1783. currentScope = currentScope.upper;
  1784. }
  1785. if (currentScope !== scope) {
  1786. // This reference is outside the Hook callback.
  1787. // It can only be legit if it's the deps array.
  1788. if (!isAncestorNodeOf(declaredDependenciesNode, reference.identifier)) {
  1789. return true;
  1790. }
  1791. }
  1792. }
  1793. return false;
  1794. }
  1795. return constructions.map(function (_ref11) {
  1796. var ref = _ref11[0],
  1797. depType = _ref11[1];
  1798. return {
  1799. construction: ref.defs[0],
  1800. depType: depType,
  1801. isUsedOutsideOfHook: isUsedOutsideOfHook(ref)
  1802. };
  1803. });
  1804. }
  1805. /**
  1806. * Assuming () means the passed/returned node:
  1807. * (props) => (props)
  1808. * props.(foo) => (props.foo)
  1809. * props.foo.(bar) => (props).foo.bar
  1810. * props.foo.bar.(baz) => (props).foo.bar.baz
  1811. */
  1812. function getDependency(node) {
  1813. if ((node.parent.type === 'MemberExpression' || node.parent.type === 'OptionalMemberExpression') && node.parent.object === node && node.parent.property.name !== 'current' && !node.parent.computed && !(node.parent.parent != null && (node.parent.parent.type === 'CallExpression' || node.parent.parent.type === 'OptionalCallExpression') && node.parent.parent.callee === node.parent)) {
  1814. return getDependency(node.parent);
  1815. } else if ( // Note: we don't check OptionalMemberExpression because it can't be LHS.
  1816. node.type === 'MemberExpression' && node.parent && node.parent.type === 'AssignmentExpression' && node.parent.left === node) {
  1817. return node.object;
  1818. } else {
  1819. return node;
  1820. }
  1821. }
  1822. /**
  1823. * Mark a node as either optional or required.
  1824. * Note: If the node argument is an OptionalMemberExpression, it doesn't necessarily mean it is optional.
  1825. * It just means there is an optional member somewhere inside.
  1826. * This particular node might still represent a required member, so check .optional field.
  1827. */
  1828. function markNode(node, optionalChains, result) {
  1829. if (optionalChains) {
  1830. if (node.optional) {
  1831. // We only want to consider it optional if *all* usages were optional.
  1832. if (!optionalChains.has(result)) {
  1833. // Mark as (maybe) optional. If there's a required usage, this will be overridden.
  1834. optionalChains.set(result, true);
  1835. }
  1836. } else {
  1837. // Mark as required.
  1838. optionalChains.set(result, false);
  1839. }
  1840. }
  1841. }
  1842. /**
  1843. * Assuming () means the passed node.
  1844. * (foo) -> 'foo'
  1845. * foo(.)bar -> 'foo.bar'
  1846. * foo.bar(.)baz -> 'foo.bar.baz'
  1847. * Otherwise throw.
  1848. */
  1849. function analyzePropertyChain(node, optionalChains) {
  1850. if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
  1851. var result = node.name;
  1852. if (optionalChains) {
  1853. // Mark as required.
  1854. optionalChains.set(result, false);
  1855. }
  1856. return result;
  1857. } else if (node.type === 'MemberExpression' && !node.computed) {
  1858. var object = analyzePropertyChain(node.object, optionalChains);
  1859. var property = analyzePropertyChain(node.property, null);
  1860. var _result = object + "." + property;
  1861. markNode(node, optionalChains, _result);
  1862. return _result;
  1863. } else if (node.type === 'OptionalMemberExpression' && !node.computed) {
  1864. var _object = analyzePropertyChain(node.object, optionalChains);
  1865. var _property = analyzePropertyChain(node.property, null);
  1866. var _result2 = _object + "." + _property;
  1867. markNode(node, optionalChains, _result2);
  1868. return _result2;
  1869. } else if (node.type === 'ChainExpression' && !node.computed) {
  1870. var expression = node.expression;
  1871. if (expression.type === 'CallExpression') {
  1872. throw new Error("Unsupported node type: " + expression.type);
  1873. }
  1874. var _object2 = analyzePropertyChain(expression.object, optionalChains);
  1875. var _property2 = analyzePropertyChain(expression.property, null);
  1876. var _result3 = _object2 + "." + _property2;
  1877. markNode(expression, optionalChains, _result3);
  1878. return _result3;
  1879. } else {
  1880. throw new Error("Unsupported node type: " + node.type);
  1881. }
  1882. }
  1883. function getNodeWithoutReactNamespace(node, options) {
  1884. if (node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.object.name === 'React' && node.property.type === 'Identifier' && !node.computed) {
  1885. return node.property;
  1886. }
  1887. return node;
  1888. } // What's the index of callback that needs to be analyzed for a given Hook?
  1889. // -1 if it's not a Hook we care about (e.g. useState).
  1890. // 0 for useEffect/useMemo/useCallback(fn).
  1891. // 1 for useImperativeHandle(ref, fn).
  1892. // For additionally configured Hooks, assume that they're like useEffect (0).
  1893. function getReactiveHookCallbackIndex(calleeNode, options) {
  1894. var node = getNodeWithoutReactNamespace(calleeNode);
  1895. if (node.type !== 'Identifier') {
  1896. return -1;
  1897. }
  1898. switch (node.name) {
  1899. case 'useEffect':
  1900. case 'useLayoutEffect':
  1901. case 'useCallback':
  1902. case 'useMemo':
  1903. // useEffect(fn)
  1904. return 0;
  1905. case 'useImperativeHandle':
  1906. // useImperativeHandle(ref, fn)
  1907. return 1;
  1908. default:
  1909. if (node === calleeNode && options && options.additionalHooks) {
  1910. // Allow the user to provide a regular expression which enables the lint to
  1911. // target custom reactive hooks.
  1912. var name;
  1913. try {
  1914. name = analyzePropertyChain(node, null);
  1915. } catch (error) {
  1916. if (/Unsupported node type/.test(error.message)) {
  1917. return 0;
  1918. } else {
  1919. throw error;
  1920. }
  1921. }
  1922. return options.additionalHooks.test(name) ? 0 : -1;
  1923. } else {
  1924. return -1;
  1925. }
  1926. }
  1927. }
  1928. /**
  1929. * ESLint won't assign node.parent to references from context.getScope()
  1930. *
  1931. * So instead we search for the node from an ancestor assigning node.parent
  1932. * as we go. This mutates the AST.
  1933. *
  1934. * This traversal is:
  1935. * - optimized by only searching nodes with a range surrounding our target node
  1936. * - agnostic to AST node types, it looks for `{ type: string, ... }`
  1937. */
  1938. function fastFindReferenceWithParent(start, target) {
  1939. var queue = [start];
  1940. var item = null;
  1941. while (queue.length) {
  1942. item = queue.shift();
  1943. if (isSameIdentifier(item, target)) {
  1944. return item;
  1945. }
  1946. if (!isAncestorNodeOf(item, target)) {
  1947. continue;
  1948. }
  1949. for (var _i4 = 0, _Object$entries = Object.entries(item); _i4 < _Object$entries.length; _i4++) {
  1950. var _Object$entries$_i = _Object$entries[_i4],
  1951. key = _Object$entries$_i[0],
  1952. value = _Object$entries$_i[1];
  1953. if (key === 'parent') {
  1954. continue;
  1955. }
  1956. if (isNodeLike(value)) {
  1957. value.parent = item;
  1958. queue.push(value);
  1959. } else if (Array.isArray(value)) {
  1960. value.forEach(function (val) {
  1961. if (isNodeLike(val)) {
  1962. val.parent = item;
  1963. queue.push(val);
  1964. }
  1965. });
  1966. }
  1967. }
  1968. }
  1969. return null;
  1970. }
  1971. function joinEnglish(arr) {
  1972. var s = '';
  1973. for (var i = 0; i < arr.length; i++) {
  1974. s += arr[i];
  1975. if (i === 0 && arr.length === 2) {
  1976. s += ' and ';
  1977. } else if (i === arr.length - 2 && arr.length > 2) {
  1978. s += ', and ';
  1979. } else if (i < arr.length - 1) {
  1980. s += ', ';
  1981. }
  1982. }
  1983. return s;
  1984. }
  1985. function isNodeLike(val) {
  1986. return typeof val === 'object' && val !== null && !Array.isArray(val) && typeof val.type === 'string';
  1987. }
  1988. function isSameIdentifier(a, b) {
  1989. return (a.type === 'Identifier' || a.type === 'JSXIdentifier') && a.type === b.type && a.name === b.name && a.range[0] === b.range[0] && a.range[1] === b.range[1];
  1990. }
  1991. function isAncestorNodeOf(a, b) {
  1992. return a.range[0] <= b.range[0] && a.range[1] >= b.range[1];
  1993. }
  1994. var configs = {
  1995. recommended: {
  1996. plugins: ['react-hooks'],
  1997. rules: {
  1998. 'react-hooks/rules-of-hooks': 'error',
  1999. 'react-hooks/exhaustive-deps': 'warn'
  2000. }
  2001. }
  2002. };
  2003. var rules = {
  2004. 'rules-of-hooks': RulesOfHooks,
  2005. 'exhaustive-deps': ExhaustiveDeps
  2006. };
  2007. exports.configs = configs;
  2008. exports.rules = rules;
  2009. })();
  2010. }