@colrealpro/react-luau-doctor 0.19.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -11,7 +11,7 @@ var package_default = {
11
11
  publishConfig: {
12
12
  access: "public"
13
13
  },
14
- version: "0.19.0",
14
+ version: "0.19.1",
15
15
  description: "Static analysis for React-Luau hooks, effects, rendering, and performance.",
16
16
  license: "MIT",
17
17
  type: "module",
@@ -1071,6 +1071,7 @@ function serializeProjectModel(project) {
1071
1071
  bindingCompatibleComponentProps: [...project.bindingCompatibleComponentProps].map(([key, value]) => [key, [...value].sort()]),
1072
1072
  staticIterationTables: [...project.staticIterationTables].map(([key, value]) => [key, [...value].sort()]),
1073
1073
  conditionalHookModes: [...project.conditionalHookModes],
1074
+ reactHookModules: [...project.reactHookModules],
1074
1075
  sourceEffects: [...project.sourceEffects].map(([key, value]) => [key, serializeSourceEffect(value)])
1075
1076
  };
1076
1077
  }
@@ -1083,6 +1084,7 @@ function deserializeProjectModel(project) {
1083
1084
  bindingCompatibleComponentProps: new Map(project.bindingCompatibleComponentProps.map(([key, value]) => [key, new Set(value)])),
1084
1085
  staticIterationTables: new Map(project.staticIterationTables.map(([key, value]) => [key, new Set(value)])),
1085
1086
  conditionalHookModes: new Map(project.conditionalHookModes),
1087
+ reactHookModules: new Map(project.reactHookModules ?? []),
1086
1088
  sourceEffects: new Map(project.sourceEffects.map(([key, value]) => [key, deserializeSourceEffect(value)]))
1087
1089
  };
1088
1090
  }
@@ -1107,7 +1109,8 @@ function projectModelFeatureKey(project) {
1107
1109
  bindingApiAlternatives: serialized.bindingApiAlternatives,
1108
1110
  bindingCompatibleComponentProps: serialized.bindingCompatibleComponentProps,
1109
1111
  staticIterationTables: serialized.staticIterationTables,
1110
- conditionalHookModes: serialized.conditionalHookModes
1112
+ conditionalHookModes: serialized.conditionalHookModes,
1113
+ reactHookModules: serialized.reactHookModules
1111
1114
  });
1112
1115
  }
1113
1116
  function cachedProjectModel(session) {
@@ -1198,6 +1201,19 @@ function walk(node) {
1198
1201
  }
1199
1202
  return result;
1200
1203
  }
1204
+ function parameterBindingNames(parameters) {
1205
+ if (!parameters)
1206
+ return [];
1207
+ const result = [];
1208
+ for (const parameter of parameters.namedChildren) {
1209
+ if (parameter.type !== "parameter")
1210
+ continue;
1211
+ const identifier = parameter.namedChildren.find((child) => child.type === "identifier");
1212
+ if (identifier)
1213
+ result.push(identifier.text);
1214
+ }
1215
+ return [...new Set(result)];
1216
+ }
1201
1217
  function rootIdentifier(text) {
1202
1218
  const match = text.trim().match(/^([A-Za-z_][A-Za-z0-9_]*)/);
1203
1219
  return match?.[1] ?? null;
@@ -1326,6 +1342,63 @@ function createRuleContext(file) {
1326
1342
  };
1327
1343
  }
1328
1344
 
1345
+ // src/module-resolution.ts
1346
+ import path5 from "path";
1347
+ function normalizeRelative2(value) {
1348
+ return value.split(path5.sep).join("/");
1349
+ }
1350
+ function moduleKeys(relativePath) {
1351
+ let normalized = normalizeRelative2(relativePath).replace(/\.(?:lua|luau)$/i, "");
1352
+ if (normalized.endsWith("/init"))
1353
+ normalized = normalized.slice(0, -"/init".length);
1354
+ const segments = normalized.split("/").filter(Boolean);
1355
+ if (segments[0]?.toLowerCase() === "src")
1356
+ segments.shift();
1357
+ const keys = new Set;
1358
+ for (let index = 0;index < segments.length; index += 1) {
1359
+ const suffix = segments.slice(index).join(".").toLowerCase();
1360
+ if (suffix)
1361
+ keys.add(suffix);
1362
+ }
1363
+ return [...keys];
1364
+ }
1365
+ function normalizeRequireTarget(text) {
1366
+ return (text.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? []).join(".").toLowerCase();
1367
+ }
1368
+ function buildUniqueFeatureAliases(modules, featureByModuleId) {
1369
+ const owners = new Map;
1370
+ for (const module of modules) {
1371
+ for (const key of module.keys) {
1372
+ const ids = owners.get(key) ?? new Set;
1373
+ ids.add(module.id);
1374
+ owners.set(key, ids);
1375
+ }
1376
+ }
1377
+ const aliases = new Map;
1378
+ for (const module of modules) {
1379
+ const value = featureByModuleId.get(module.id);
1380
+ if (value === undefined)
1381
+ continue;
1382
+ for (const key of module.keys) {
1383
+ const ids = owners.get(key);
1384
+ if (ids?.size === 1 && ids.has(module.id))
1385
+ aliases.set(key, value);
1386
+ }
1387
+ }
1388
+ return aliases;
1389
+ }
1390
+ function resolveModuleReference(target, aliases) {
1391
+ let candidate = target;
1392
+ while (true) {
1393
+ if (aliases.has(candidate))
1394
+ return aliases.get(candidate);
1395
+ const separator = candidate.indexOf(".");
1396
+ if (separator < 0)
1397
+ return null;
1398
+ candidate = candidate.slice(separator + 1);
1399
+ }
1400
+ }
1401
+
1329
1402
  // src/ast/react-model.ts
1330
1403
  var REACT_HOOKS = new Set([
1331
1404
  "useState",
@@ -1356,18 +1429,7 @@ var INSTANCE_FACTORY_MEMBERS = new Set([
1356
1429
  ]);
1357
1430
  var STABLE_ENGINE_FACTORY_MEMBERS = new Set(["GetMouse", "GetService"]);
1358
1431
  function identifiersFromParameters(node) {
1359
- if (!node)
1360
- return [];
1361
- const result = [];
1362
- for (const child of walk(node)) {
1363
- if (child.type !== "identifier")
1364
- continue;
1365
- const parentType = child.parent?.type;
1366
- if (parentType === "parameters" || parentType === "parameter" || parentType === "typed_identifier") {
1367
- result.push(child.text);
1368
- }
1369
- }
1370
- return [...new Set(result)];
1432
+ return parameterBindingNames(node);
1371
1433
  }
1372
1434
  function getFunctionName(node) {
1373
1435
  if (node.type === "function_declaration") {
@@ -1421,15 +1483,19 @@ function directCalls(node) {
1421
1483
  function hasDirectCreateElementCall(node, aliases, reactNamespaces, reactRobloxNamespaces) {
1422
1484
  return directCalls(node).some((call) => {
1423
1485
  const name = call.childForFieldName("name")?.text ?? "";
1424
- const path5 = resolvePath(name, aliases, reactNamespaces, reactRobloxNamespaces);
1425
- return path5 === "React.createElement" || path5 === "ReactRoblox.createPortal";
1486
+ const path6 = resolvePath(name, aliases, reactNamespaces, reactRobloxNamespaces);
1487
+ return path6 === "React.createElement" || path6 === "ReactRoblox.createPortal";
1426
1488
  });
1427
1489
  }
1428
- function hasDirectHookCall(node, aliases, reactNamespaces, reactRobloxNamespaces) {
1490
+ function hasDirectHookCall(node, aliases, reactNamespaces, reactRobloxNamespaces, importedHookKinds) {
1429
1491
  return directCalls(node).some((call) => {
1430
1492
  const name = call.childForFieldName("name")?.text ?? "";
1431
- const path5 = resolvePath(name, aliases, reactNamespaces, reactRobloxNamespaces);
1432
- const final = path5.split(/[.:]/).at(-1) ?? "";
1493
+ const path6 = resolvePath(name, aliases, reactNamespaces, reactRobloxNamespaces);
1494
+ const normalizedName = normalizeExpressionText(name);
1495
+ const importedKind = importedHookKinds.get(normalizedName);
1496
+ if (importedKind !== undefined)
1497
+ return importedKind;
1498
+ const final = path6.split(/[.:]/).at(-1) ?? "";
1433
1499
  return /^use[A-Z0-9_]/.test(final);
1434
1500
  });
1435
1501
  }
@@ -1515,8 +1581,8 @@ function isSyntacticallyStableInitializer(node) {
1515
1581
  return false;
1516
1582
  return ["nil", "true", "false", "number", "string", "string_content"].includes(node.type) || /^(?:nil|true|false|-?\d+(?:\.\d+)?(?:e[+-]?\d+)?)$/i.test(node.text.trim());
1517
1583
  }
1518
- function finalCallMember(path5) {
1519
- return path5.split(/[.:]/).at(-1) ?? path5;
1584
+ function finalCallMember(path6) {
1585
+ return path6.split(/[.:]/).at(-1) ?? path6;
1520
1586
  }
1521
1587
  function expressionCreatesInstance(node, aliases, reactNamespaces, reactRobloxNamespaces) {
1522
1588
  if (!node)
@@ -1528,8 +1594,8 @@ function expressionCreatesInstance(node, aliases, reactNamespaces, reactRobloxNa
1528
1594
  if (candidate.type !== "function_call")
1529
1595
  continue;
1530
1596
  const rawPath = candidate.childForFieldName("name")?.text ?? "";
1531
- const path5 = resolvePath(rawPath, aliases, reactNamespaces, reactRobloxNamespaces);
1532
- if (path5 === "Instance.new" || INSTANCE_FACTORY_MEMBERS.has(finalCallMember(path5)))
1597
+ const path6 = resolvePath(rawPath, aliases, reactNamespaces, reactRobloxNamespaces);
1598
+ if (path6 === "Instance.new" || INSTANCE_FACTORY_MEMBERS.has(finalCallMember(path6)))
1533
1599
  return true;
1534
1600
  }
1535
1601
  return false;
@@ -1541,8 +1607,8 @@ function expressionIsStableEngineObject(node, aliases, reactNamespaces, reactRob
1541
1607
  if (calls.length !== 1)
1542
1608
  return false;
1543
1609
  const rawPath = calls[0].childForFieldName("name")?.text ?? "";
1544
- const path5 = resolvePath(rawPath, aliases, reactNamespaces, reactRobloxNamespaces);
1545
- return STABLE_ENGINE_FACTORY_MEMBERS.has(finalCallMember(path5));
1610
+ const path6 = resolvePath(rawPath, aliases, reactNamespaces, reactRobloxNamespaces);
1611
+ return STABLE_ENGINE_FACTORY_MEMBERS.has(finalCallMember(path6));
1546
1612
  }
1547
1613
  function memoReturnsInstance(callback, aliases, reactNamespaces, reactRobloxNamespaces) {
1548
1614
  if (!callback || callback.type !== "function_definition")
@@ -1558,7 +1624,21 @@ function memoReturnsInstance(callback, aliases, reactNamespaces, reactRobloxName
1558
1624
  function isEmptyDependencyTable(node) {
1559
1625
  return Boolean(node?.type === "table_constructor" && node.namedChildren.every((child) => child.type !== "field"));
1560
1626
  }
1561
- function buildReactModel(root) {
1627
+ function importedHookKinds(declarations, project) {
1628
+ const result = new Map;
1629
+ if (!project)
1630
+ return result;
1631
+ for (const declaration of declarations) {
1632
+ const match = declaration.text.match(/^\s*local\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*require\s*\((.*?)\)\s*$/s);
1633
+ if (!match || !/^use[A-Z0-9_]/.test(match[1]))
1634
+ continue;
1635
+ const kind = resolveModuleReference(normalizeRequireTarget(match[2]), project.reactHookModules);
1636
+ if (kind !== null)
1637
+ result.set(match[1], kind);
1638
+ }
1639
+ return result;
1640
+ }
1641
+ function buildReactModel(root, project) {
1562
1642
  const reactNamespaces = new Set;
1563
1643
  const reactRobloxNamespaces = new Set;
1564
1644
  const aliases = new Map;
@@ -1574,6 +1654,7 @@ function buildReactModel(root) {
1574
1654
  const instanceVariablesByFunction = new Map;
1575
1655
  const allNodes = [...walk(root)];
1576
1656
  const declarations = allNodes.filter((node) => node.type === "variable_declaration");
1657
+ const projectHookKinds = importedHookKinds(declarations, project);
1577
1658
  let isReactFile = false;
1578
1659
  for (const declaration of declarations) {
1579
1660
  const text = declaration.text;
@@ -1648,7 +1729,7 @@ function buildReactModel(root) {
1648
1729
  if (!isReactFile || !info.body || info.isHook)
1649
1730
  continue;
1650
1731
  const simplePascalName = Boolean(info.name && /^[A-Z][A-Za-z0-9_]*$/.test(info.name));
1651
- const hasHooks = hasDirectHookCall(info.body, aliases, reactNamespaces, reactRobloxNamespaces);
1732
+ const hasHooks = hasDirectHookCall(info.body, aliases, reactNamespaces, reactRobloxNamespaces, projectHookKinds);
1652
1733
  const renders = hasDirectCreateElementCall(info.body, aliases, reactNamespaces, reactRobloxNamespaces);
1653
1734
  if (hasHooks || isMemoWrappedFunction(info.node, aliases, reactNamespaces) || simplePascalName && renders) {
1654
1735
  info.isComponent = true;
@@ -1660,8 +1741,8 @@ function buildReactModel(root) {
1660
1741
  if (call.type !== "function_call")
1661
1742
  continue;
1662
1743
  const rawPath = call.childForFieldName("name")?.text ?? "";
1663
- const path5 = resolvePath(rawPath, aliases, reactNamespaces, reactRobloxNamespaces);
1664
- if (path5 !== "React.createElement")
1744
+ const path6 = resolvePath(rawPath, aliases, reactNamespaces, reactRobloxNamespaces);
1745
+ if (path6 !== "React.createElement")
1665
1746
  continue;
1666
1747
  const element = call.childForFieldName("arguments")?.namedChildren[0]?.text.trim() ?? "";
1667
1748
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(element))
@@ -1696,13 +1777,13 @@ function buildReactModel(root) {
1696
1777
  if (!call)
1697
1778
  continue;
1698
1779
  const rawPath = call.childForFieldName("name")?.text ?? "";
1699
- const path5 = resolvePath(rawPath, aliases, reactNamespaces, reactRobloxNamespaces);
1780
+ const path6 = resolvePath(rawPath, aliases, reactNamespaces, reactRobloxNamespaces);
1700
1781
  const args = call.childForFieldName("arguments")?.namedChildren ?? [];
1701
- if (path5 === "React.joinBindings" && names.length >= 1) {
1782
+ if (path6 === "React.joinBindings" && names.length >= 1) {
1702
1783
  stableVariables.add(names[0]);
1703
1784
  addScopedVariable(stableVariablesByFunction, owner, names[0]);
1704
1785
  addScopedVariable(externalMutableVariablesByFunction, owner, names[0]);
1705
- } else if (path5 === "React.useState" && names.length >= 2) {
1786
+ } else if (path6 === "React.useState" && names.length >= 2) {
1706
1787
  const [valueName, setterName] = names;
1707
1788
  stateSetters.set(setterName, valueName);
1708
1789
  stateValues.set(valueName, setterName);
@@ -1716,10 +1797,10 @@ function buildReactModel(root) {
1716
1797
  initializer: args[0] ?? null,
1717
1798
  owner
1718
1799
  });
1719
- } else if (path5 === "React.useReducer" && names.length >= 2) {
1800
+ } else if (path6 === "React.useReducer" && names.length >= 2) {
1720
1801
  stableVariables.add(names[1]);
1721
1802
  addScopedVariable(stableVariablesByFunction, owner, names[1]);
1722
- } else if (path5 === "React.useBinding" && names.length >= 1) {
1803
+ } else if (path6 === "React.useBinding" && names.length >= 1) {
1723
1804
  stableVariables.add(names[0]);
1724
1805
  addScopedVariable(stableVariablesByFunction, owner, names[0]);
1725
1806
  if (names.length >= 2) {
@@ -1727,7 +1808,7 @@ function buildReactModel(root) {
1727
1808
  bindingSetters.add(names[1]);
1728
1809
  addScopedVariable(stableVariablesByFunction, owner, names[1]);
1729
1810
  }
1730
- } else if (path5 === "React.useMemo" && names.length >= 1) {
1811
+ } else if (path6 === "React.useMemo" && names.length >= 1) {
1731
1812
  if (isEmptyDependencyTable(args[1])) {
1732
1813
  stableVariables.add(names[0]);
1733
1814
  addScopedVariable(stableVariablesByFunction, owner, names[0]);
@@ -1736,16 +1817,16 @@ function buildReactModel(root) {
1736
1817
  addScopedVariable(externalMutableVariablesByFunction, owner, names[0]);
1737
1818
  addScopedVariable(instanceVariablesByFunction, owner, names[0]);
1738
1819
  }
1739
- } else if (path5 === "React.useCallback" && names.length >= 1 && isEmptyDependencyTable(args[1])) {
1820
+ } else if (path6 === "React.useCallback" && names.length >= 1 && isEmptyDependencyTable(args[1])) {
1740
1821
  stableVariables.add(names[0]);
1741
1822
  addScopedVariable(stableVariablesByFunction, owner, names[0]);
1742
- } else if (path5 === "React.useRef" && names.length >= 1) {
1823
+ } else if (path6 === "React.useRef" && names.length >= 1) {
1743
1824
  stableVariables.add(names[0]);
1744
1825
  refVariables.add(names[0]);
1745
1826
  addScopedVariable(stableVariablesByFunction, owner, names[0]);
1746
1827
  addScopedVariable(refVariablesByFunction, owner, names[0]);
1747
1828
  } else {
1748
- const final = path5.split(/[.:]/).at(-1) ?? "";
1829
+ const final = path6.split(/[.:]/).at(-1) ?? "";
1749
1830
  if (/^use[A-Z0-9_]/.test(final)) {
1750
1831
  for (const name of names.slice(1)) {
1751
1832
  if (!STABLE_CUSTOM_HOOK_RETURN.test(name))
@@ -1769,11 +1850,11 @@ function buildReactModel(root) {
1769
1850
  if (!expression || expression.type !== "function_call")
1770
1851
  continue;
1771
1852
  const rawPath = expression.childForFieldName("name")?.text ?? "";
1772
- const path5 = resolvePath(rawPath, aliases, reactNamespaces, reactRobloxNamespaces);
1773
- if (path5 === "React.useState" && names.length >= 2) {
1853
+ const path6 = resolvePath(rawPath, aliases, reactNamespaces, reactRobloxNamespaces);
1854
+ if (path6 === "React.useState" && names.length >= 2) {
1774
1855
  stableVariables.add(names[1]);
1775
1856
  addScopedVariable(stableVariablesByFunction, owner, names[1]);
1776
- } else if (path5 === "React.useBinding" && names.length >= 1) {
1857
+ } else if (path6 === "React.useBinding" && names.length >= 1) {
1777
1858
  stableVariables.add(names[0]);
1778
1859
  addScopedVariable(stableVariablesByFunction, owner, names[0]);
1779
1860
  addScopedVariable(externalMutableVariablesByFunction, owner, names[0]);
@@ -1888,7 +1969,7 @@ function createInlineSuppressionChecker(source) {
1888
1969
 
1889
1970
  // src/parser.ts
1890
1971
  import fs5 from "fs";
1891
- import path5 from "path";
1972
+ import path6 from "path";
1892
1973
  import { Language, Parser } from "web-tree-sitter";
1893
1974
 
1894
1975
  // src/parser-compat.ts
@@ -2027,6 +2108,23 @@ function isCode(mask, start, end = start + 1) {
2027
2108
  }
2028
2109
  return true;
2029
2110
  }
2111
+ function matchingCodeDelimiterEnd(source, mask, start, opener, closer) {
2112
+ if (source[start] !== opener || !isCode(mask, start))
2113
+ return null;
2114
+ let depth = 0;
2115
+ for (let cursor = start;cursor < source.length; cursor += 1) {
2116
+ if (!isCode(mask, cursor))
2117
+ continue;
2118
+ if (source[cursor] === opener)
2119
+ depth += 1;
2120
+ else if (source[cursor] === closer) {
2121
+ depth -= 1;
2122
+ if (depth === 0)
2123
+ return cursor + 1;
2124
+ }
2125
+ }
2126
+ return null;
2127
+ }
2030
2128
  function maskTypeAliases(source, mask) {
2031
2129
  const { lines: lines2, offsets } = lineData(source);
2032
2130
  const ranges = [];
@@ -2234,6 +2332,89 @@ function genericFunctionParameterRanges(source, mask) {
2234
2332
  }
2235
2333
  return ranges;
2236
2334
  }
2335
+ function genericFunctionReturnPrefixRanges(source, mask) {
2336
+ const ranges = [];
2337
+ const functionPattern = /\bfunction\b/g;
2338
+ for (const match of source.matchAll(functionPattern)) {
2339
+ const start = match.index;
2340
+ if (!isCode(mask, start, start + match[0].length))
2341
+ continue;
2342
+ let cursor = start + match[0].length;
2343
+ let genericDepth = 0;
2344
+ while (cursor < source.length) {
2345
+ if (!isCode(mask, cursor)) {
2346
+ cursor += 1;
2347
+ continue;
2348
+ }
2349
+ if (source[cursor] === "<")
2350
+ genericDepth += 1;
2351
+ else if (source[cursor] === ">" && genericDepth > 0)
2352
+ genericDepth -= 1;
2353
+ else if (source[cursor] === "(" && genericDepth === 0)
2354
+ break;
2355
+ cursor += 1;
2356
+ }
2357
+ if (source[cursor] !== "(")
2358
+ continue;
2359
+ const parameterEnd = matchingCodeDelimiterEnd(source, mask, cursor, "(", ")");
2360
+ if (parameterEnd === null)
2361
+ continue;
2362
+ cursor = parameterEnd;
2363
+ while (cursor < source.length && /\s/.test(source[cursor]))
2364
+ cursor += 1;
2365
+ if (source[cursor] !== ":" || !isCode(mask, cursor))
2366
+ continue;
2367
+ cursor += 1;
2368
+ while (cursor < source.length && /\s/.test(source[cursor]))
2369
+ cursor += 1;
2370
+ if (source[cursor] !== "<")
2371
+ continue;
2372
+ const rangeStart = cursor;
2373
+ const genericEnd = matchingCodeDelimiterEnd(source, mask, cursor, "<", ">");
2374
+ if (genericEnd === null)
2375
+ continue;
2376
+ cursor = genericEnd;
2377
+ while (cursor < source.length && /\s/.test(source[cursor]))
2378
+ cursor += 1;
2379
+ const typeParametersEnd = matchingCodeDelimiterEnd(source, mask, cursor, "(", ")");
2380
+ if (typeParametersEnd === null)
2381
+ continue;
2382
+ cursor = typeParametersEnd;
2383
+ while (cursor < source.length && /\s/.test(source[cursor]))
2384
+ cursor += 1;
2385
+ if (source.slice(cursor, cursor + 2) !== "->" || !isCode(mask, cursor, cursor + 2))
2386
+ continue;
2387
+ cursor += 2;
2388
+ while (cursor < source.length && /[ \t]/.test(source[cursor]))
2389
+ cursor += 1;
2390
+ ranges.push({ start: rangeStart, end: cursor });
2391
+ }
2392
+ return ranges;
2393
+ }
2394
+ function normalizeLocalTypeofAnnotations(source, mask) {
2395
+ const replacements = [];
2396
+ const pattern = /\blocal\s+[A-Za-z_][A-Za-z0-9_]*\s*:\s*typeof\s*\(/g;
2397
+ for (const match of source.matchAll(pattern)) {
2398
+ const matchStart = match.index;
2399
+ if (!isCode(mask, matchStart, matchStart + match[0].length))
2400
+ continue;
2401
+ const typeofStart = matchStart + match[0].lastIndexOf("typeof");
2402
+ const open = source.indexOf("(", typeofStart + 6);
2403
+ if (open < 0)
2404
+ continue;
2405
+ const end = matchingCodeDelimiterEnd(source, mask, open, "(", ")");
2406
+ if (end === null)
2407
+ continue;
2408
+ const original = source.slice(typeofStart, end);
2409
+ const padding = blankPreservingUtf8Bytes(original).slice(3);
2410
+ replacements.push({ start: typeofStart, end, value: `any${padding}` });
2411
+ }
2412
+ let output = source;
2413
+ for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
2414
+ output = `${output.slice(0, replacement.start)}${replacement.value}${output.slice(replacement.end)}`;
2415
+ }
2416
+ return output;
2417
+ }
2237
2418
  function normalizeGenericTypePackUses(source, mask) {
2238
2419
  const declarationRanges = genericFunctionParameterRanges(source, mask);
2239
2420
  const replacements = [];
@@ -2294,13 +2475,15 @@ function parserCompatibleSource(source) {
2294
2475
  const keywordCompatible = replaceContextualKeywords(source, mask);
2295
2476
  const literalCompatible = normalizeIntegerLiteralSuffixes(keywordCompatible, mask);
2296
2477
  const packCompatible = normalizeGenericTypePackUses(literalCompatible, mask);
2478
+ const typeofCompatible = normalizeLocalTypeofAnnotations(packCompatible, mask);
2297
2479
  const ranges = [
2298
2480
  ...maskTypeAliases(source, mask),
2299
2481
  ...maskTypeLevelBlocks(source, mask),
2300
2482
  ...attributeRanges(source, mask),
2301
- ...explicitTypeArgumentRanges(source, mask)
2483
+ ...explicitTypeArgumentRanges(source, mask),
2484
+ ...genericFunctionReturnPrefixRanges(source, mask)
2302
2485
  ];
2303
- const masked = applyMasks(packCompatible, ranges);
2486
+ const masked = applyMasks(typeofCompatible, ranges);
2304
2487
  return masked.replace(/^(\s*)type(?=\s*(?:\+=|-=|\*=|\/=|%=|\^=|\.\.=|=(?!=)))/gm, "$1_typ");
2305
2488
  }
2306
2489
 
@@ -2308,7 +2491,7 @@ function parserCompatibleSource(source) {
2308
2491
  var parserPromise = null;
2309
2492
  async function createParser() {
2310
2493
  await Parser.init();
2311
- const grammarPath = path5.resolve(import.meta.dir, "../vendor/tree-sitter-luau.wasm");
2494
+ const grammarPath = path6.resolve(import.meta.dir, "../vendor/tree-sitter-luau.wasm");
2312
2495
  const grammar = new Uint8Array(fs5.readFileSync(grammarPath));
2313
2496
  const language = await Language.load(grammar);
2314
2497
  const parser = new Parser;
@@ -2433,8 +2616,8 @@ var INSTANCE_FACTORY_MEMBERS2 = new Set([
2433
2616
  "GetService",
2434
2617
  "WaitForChild"
2435
2618
  ]);
2436
- function finalCallMember2(path6) {
2437
- return path6.split(/[.:]/).at(-1) ?? path6;
2619
+ function finalCallMember2(path7) {
2620
+ return path7.split(/[.:]/).at(-1) ?? path7;
2438
2621
  }
2439
2622
  function declarationInitializer(node, name) {
2440
2623
  if (node.type !== "variable_declaration")
@@ -2502,8 +2685,8 @@ function expressionHasRobloxOrigin(expression, from, context, seen) {
2502
2685
  seen.add(root);
2503
2686
  return expressionHasRobloxOrigin(visibleInitializer(root, from, context), from, context, seen);
2504
2687
  }
2505
- function hasRobloxReceiver(path6, call, context) {
2506
- const normalized = path6.replace(/\s+/g, "");
2688
+ function hasRobloxReceiver(path7, call, context) {
2689
+ const normalized = path7.replace(/\s+/g, "");
2507
2690
  if (/^(?:game|workspace|script)(?:[.:]|$)/.test(normalized))
2508
2691
  return true;
2509
2692
  const root = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*)/)?.[1];
@@ -2652,8 +2835,8 @@ function sourceHasHighFrequencyRobloxEvent(source) {
2652
2835
  }
2653
2836
  return false;
2654
2837
  }
2655
- function knownYieldReason(path6, call, context) {
2656
- const normalized = path6.replace(/\s+/g, "");
2838
+ function knownYieldReason(path7, call, context) {
2839
+ const normalized = path7.replace(/\s+/g, "");
2657
2840
  if (normalized === "task.wait")
2658
2841
  return "task.wait";
2659
2842
  if (normalized === "coroutine.yield")
@@ -2673,15 +2856,15 @@ function functionYieldPoint(node, context) {
2673
2856
  const owner = context.model.functionByNode.get(node.id);
2674
2857
  if (owner && nearest !== owner)
2675
2858
  continue;
2676
- const path6 = context.resolveCallPath(context.getCallPath(candidate) ?? "");
2677
- const reason = knownYieldReason(path6, candidate, context);
2859
+ const path7 = context.resolveCallPath(context.getCallPath(candidate) ?? "");
2860
+ const reason = knownYieldReason(path7, candidate, context);
2678
2861
  if (reason)
2679
2862
  return { call: candidate, reason };
2680
2863
  }
2681
2864
  return null;
2682
2865
  }
2683
- function mutableDependencyBase(path6, externallyMutableRoots) {
2684
- const normalized = path6.replace(/\s+/g, "");
2866
+ function mutableDependencyBase(path7, externallyMutableRoots) {
2867
+ const normalized = path7.replace(/\s+/g, "");
2685
2868
  if (/\.current(?:\.|$)/.test(normalized))
2686
2869
  return "";
2687
2870
  const segments = normalized.split(".");
@@ -2707,8 +2890,8 @@ var CONDITIONAL_TYPES = new Set([
2707
2890
  "while_statement",
2708
2891
  "repeat_statement"
2709
2892
  ]);
2710
- function isHookPath(path6) {
2711
- const final = path6.split(/[.:]/).at(-1) ?? "";
2893
+ function isHookPath(path7) {
2894
+ const final = path7.split(/[.:]/).at(-1) ?? "";
2712
2895
  return /^use[A-Z0-9_]/.test(final);
2713
2896
  }
2714
2897
  function assignmentTargetNode(statement) {
@@ -2784,8 +2967,8 @@ function functionInfoForNode(context, node) {
2784
2967
  function stateBindingsFor(context, owner) {
2785
2968
  return context.model.stateBindings.filter((binding) => binding.owner === owner);
2786
2969
  }
2787
- function isEffectPath(path6) {
2788
- return path6 === "React.useEffect" || path6 === "React.useLayoutEffect";
2970
+ function isEffectPath(path7) {
2971
+ return path7 === "React.useEffect" || path7 === "React.useLayoutEffect";
2789
2972
  }
2790
2973
  function dependencyExpressions(node) {
2791
2974
  if (!node || node.type !== "table_constructor")
@@ -3040,11 +3223,11 @@ function isManagedByCleanupContainer(call, context, callback) {
3040
3223
  let current = call.parent;
3041
3224
  while (current && !sameNode(current, callback.node)) {
3042
3225
  if (current.type === "function_call") {
3043
- const path6 = context.getCallPath(current) ?? "";
3044
- if (ALWAYS_MANAGED_METHOD.test(path6))
3226
+ const path7 = context.getCallPath(current) ?? "";
3227
+ if (ALWAYS_MANAGED_METHOD.test(path7))
3045
3228
  return true;
3046
- if (GENERIC_MANAGER_METHOD.test(path6)) {
3047
- const receiver = path6.split(":", 1)[0] ?? "";
3229
+ if (GENERIC_MANAGER_METHOD.test(path7)) {
3230
+ const receiver = path7.split(":", 1)[0] ?? "";
3048
3231
  if (MANAGER_ROOT.test(receiver))
3049
3232
  return true;
3050
3233
  }
@@ -3149,10 +3332,10 @@ function isCleanupOnlyTask(callbackNode, context) {
3149
3332
  return false;
3150
3333
  return calls.every((call) => /:(?:Destroy|Disconnect)$/.test(context.getCallPath(call) ?? ""));
3151
3334
  }
3152
- function taskSeverity(path6, call, taskCallback, yieldPoint, context) {
3153
- if (path6 === "task.defer" && !yieldPoint)
3335
+ function taskSeverity(path7, call, taskCallback, yieldPoint, context) {
3336
+ if (path7 === "task.defer" && !yieldPoint)
3154
3337
  return "suggestion";
3155
- if (path6 === "task.delay" && !yieldPoint) {
3338
+ if (path7 === "task.delay" && !yieldPoint) {
3156
3339
  if (isCleanupOnlyTask(taskCallback, context))
3157
3340
  return "suggestion";
3158
3341
  const delay = numericTaskDelay(call, context);
@@ -3202,10 +3385,10 @@ end`,
3202
3385
  };
3203
3386
  }
3204
3387
  if ([...labels].every((label) => label.includes("task"))) {
3205
- const path6 = context.resolveCallPath(context.getCallPath(first.call) ?? "task.delay");
3206
- const taskCall = path6 === "task.defer" ? `task.defer(function()
3388
+ const path7 = context.resolveCallPath(context.getCallPath(first.call) ?? "task.delay");
3389
+ const taskCall = path7 === "task.defer" ? `task.defer(function()
3207
3390
  doWork()
3208
- end)` : path6 === "task.spawn" ? `task.spawn(function()
3391
+ end)` : path7 === "task.spawn" ? `task.spawn(function()
3209
3392
  doWork()
3210
3393
  end)` : `task.delay(delaySeconds, function()
3211
3394
  doWork()
@@ -3258,9 +3441,9 @@ end`,
3258
3441
  }
3259
3442
  function classifyResource(call, context, callback) {
3260
3443
  const rawPath = context.getCallPath(call) ?? "";
3261
- const path6 = context.resolveCallPath(rawPath);
3444
+ const path7 = context.resolveCallPath(rawPath);
3262
3445
  const variable = assignedVariableName(call, callback);
3263
- const finalMember = path6.split(/[.:]/).at(-1) ?? path6;
3446
+ const finalMember = path7.split(/[.:]/).at(-1) ?? path7;
3264
3447
  if (/:Connect$/.test(rawPath)) {
3265
3448
  const ownerExpression = connectionOwnerExpression(rawPath);
3266
3449
  const ownerIsInstance = isProvenInstanceExpression(ownerExpression, call, context, callback);
@@ -3329,7 +3512,7 @@ function classifyResource(call, context, callback) {
3329
3512
  }
3330
3513
  };
3331
3514
  }
3332
- if (path6 === "ReactRoblox.createRoot") {
3515
+ if (path7 === "ReactRoblox.createRoot") {
3333
3516
  return {
3334
3517
  call,
3335
3518
  label: "React root",
@@ -3339,17 +3522,17 @@ function classifyResource(call, context, callback) {
3339
3522
  }
3340
3523
  };
3341
3524
  }
3342
- if (path6 === "task.delay" || path6 === "task.defer" || path6 === "task.spawn") {
3525
+ if (path7 === "task.delay" || path7 === "task.defer" || path7 === "task.spawn") {
3343
3526
  const taskCallback = taskCallbackNode(call, context, callback);
3344
3527
  const yieldPoint = taskCallback ? functionYieldPoint(taskCallback, context) : null;
3345
- const outlivesEffect = path6 !== "task.spawn" || Boolean(yieldPoint);
3528
+ const outlivesEffect = path7 !== "task.spawn" || Boolean(yieldPoint);
3346
3529
  if (!outlivesEffect)
3347
3530
  return null;
3348
3531
  return {
3349
3532
  call,
3350
- label: path6 === "task.spawn" ? "yielding spawned task" : path6 === "task.defer" ? "deferred task" : "delayed task",
3533
+ label: path7 === "task.spawn" ? "yielding spawned task" : path7 === "task.defer" ? "deferred task" : "delayed task",
3351
3534
  detail: yieldPoint ? `The task can outlive the effect because it yields at ${yieldPoint.reason}.` : "The task is scheduled to run after the effect callback can finish.",
3352
- severity: taskSeverity(path6, call, taskCallback, yieldPoint, context),
3535
+ severity: taskSeverity(path7, call, taskCallback, yieldPoint, context),
3353
3536
  variable,
3354
3537
  cleanupMatches(text, name) {
3355
3538
  if (name) {
@@ -3376,8 +3559,8 @@ var effectNeedsCleanup = {
3376
3559
  const rawPath = context.getCallPath(call);
3377
3560
  if (!rawPath)
3378
3561
  continue;
3379
- const path6 = context.resolveCallPath(rawPath);
3380
- if (path6 !== "React.useEffect" && path6 !== "React.useLayoutEffect")
3562
+ const path7 = context.resolveCallPath(rawPath);
3563
+ if (path7 !== "React.useEffect" && path7 !== "React.useLayoutEffect")
3381
3564
  continue;
3382
3565
  const callbackNode = firstFunctionArgument(context, call);
3383
3566
  const callback = functionInfoForNode(context, callbackNode);
@@ -3431,10 +3614,22 @@ function ownerForHook(context, call) {
3431
3614
  const owner = context.nearestFunction(call);
3432
3615
  return owner && (owner.isComponent || owner.isHook) ? owner : null;
3433
3616
  }
3617
+ function effectCallbackWithoutDeps(context, call) {
3618
+ const path7 = context.resolveCallPath(context.getCallPath(call) ?? "");
3619
+ if (!isEffectPath(path7))
3620
+ return null;
3621
+ const args = context.callArguments(call);
3622
+ const callback = args[0];
3623
+ if (callback?.type !== "function_definition")
3624
+ return null;
3625
+ if (args.length < 2 || args[1]?.type === "nil")
3626
+ return callback;
3627
+ return null;
3628
+ }
3434
3629
  function callbackAndDeps(context, call) {
3435
- const path6 = context.resolveCallPath(context.getCallPath(call) ?? "");
3630
+ const path7 = context.resolveCallPath(context.getCallPath(call) ?? "");
3436
3631
  const args = context.callArguments(call);
3437
- if (path6 === "React.useImperativeHandle") {
3632
+ if (path7 === "React.useImperativeHandle") {
3438
3633
  const callback2 = args[1];
3439
3634
  const deps2 = args[2];
3440
3635
  if (callback2?.type === "function_definition" && deps2?.type === "table_constructor")
@@ -3464,8 +3659,8 @@ function hasExternalSubscription(node, context) {
3464
3659
  for (const child of context.walk(node)) {
3465
3660
  if (child.type !== "function_call")
3466
3661
  continue;
3467
- const path6 = context.getCallPath(child) ?? "";
3468
- if (/:Connect$/.test(path6) || /BindToRenderStep$/.test(path6) || /:BindAction(?:AtPriority)?$/.test(path6))
3662
+ const path7 = context.getCallPath(child) ?? "";
3663
+ if (/:Connect$/.test(path7) || /BindToRenderStep$/.test(path7) || /:BindAction(?:AtPriority)?$/.test(path7))
3469
3664
  return true;
3470
3665
  }
3471
3666
  return false;
@@ -3499,6 +3694,33 @@ function setterCalledOutsideCallback(context, owner, callback, setterName) {
3499
3694
  }
3500
3695
  return false;
3501
3696
  }
3697
+ function guaranteedRepeatedStateChange(argument, stateName, context) {
3698
+ if (argument.type === "table_constructor")
3699
+ return true;
3700
+ if (argument.type === "function_definition") {
3701
+ const updater = functionInfoForNode(context, argument);
3702
+ const previousName = updater?.parameters[0];
3703
+ const returnStatement = updater?.body?.namedChildren.find((child) => child.type === "return_statement");
3704
+ const returnValue = returnStatement?.namedChildren[0];
3705
+ if (previousName && returnValue && guaranteedRepeatedStateChange(returnValue, previousName, context))
3706
+ return true;
3707
+ return false;
3708
+ }
3709
+ const text = argument.text.trim();
3710
+ const escaped = stateName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3711
+ if (new RegExp(`^not\\s+${escaped}$`).test(text))
3712
+ return true;
3713
+ if (new RegExp(`^${escaped}\\s*[+-]\\s*(?:[1-9]\\d*(?:\\.\\d+)?|0?\\.\\d*[1-9]\\d*)$`).test(text))
3714
+ return true;
3715
+ if (new RegExp(`^(?:[1-9]\\d*(?:\\.\\d+)?|0?\\.\\d*[1-9]\\d*)\\s*\\+\\s*${escaped}$`).test(text))
3716
+ return true;
3717
+ if (argument.type === "function_call") {
3718
+ const path7 = context.resolveCallPath(context.getCallPath(argument) ?? "");
3719
+ if (path7 === "table.clone" || path7 === "table.create" || path7 === "table.pack")
3720
+ return true;
3721
+ }
3722
+ return false;
3723
+ }
3502
3724
  function freshDependencyKind(node, owner) {
3503
3725
  if (node.type === "table_constructor")
3504
3726
  return "table";
@@ -3521,8 +3743,8 @@ var noDerivedStateEffect = {
3521
3743
  run(context) {
3522
3744
  const diagnostics = [];
3523
3745
  for (const call of context.findCalls()) {
3524
- const path6 = context.resolveCallPath(context.getCallPath(call) ?? "");
3525
- if (!isEffectPath(path6))
3746
+ const path7 = context.resolveCallPath(context.getCallPath(call) ?? "");
3747
+ if (!isEffectPath(path7))
3526
3748
  continue;
3527
3749
  const owner = ownerForHook(context, call);
3528
3750
  const parts = callbackAndDeps(context, call);
@@ -3560,28 +3782,48 @@ var noSelfUpdatingEffect = {
3560
3782
  id: "react-luau/no-self-updating-effect",
3561
3783
  category: "Effects",
3562
3784
  severity: "warning",
3563
- description: "Effects should not unconditionally update state that is also one of their dependencies.",
3785
+ description: "Effects should not unconditionally update state in a way that schedules themselves again.",
3564
3786
  run(context) {
3565
3787
  const diagnostics = [];
3566
3788
  for (const call of context.findCalls()) {
3567
- const path6 = context.resolveCallPath(context.getCallPath(call) ?? "");
3568
- if (!isEffectPath(path6))
3789
+ const path7 = context.resolveCallPath(context.getCallPath(call) ?? "");
3790
+ if (!isEffectPath(path7))
3569
3791
  continue;
3570
3792
  const owner = ownerForHook(context, call);
3793
+ if (!owner)
3794
+ continue;
3571
3795
  const parts = callbackAndDeps(context, call);
3572
- if (!owner || !parts)
3796
+ if (parts) {
3797
+ const deps = parseDependencyRoots(parts.deps);
3798
+ for (const binding of stateBindingsFor(context, owner)) {
3799
+ if (!deps.has(binding.valueName))
3800
+ continue;
3801
+ for (const setterCall of directTopLevelCalls(context, parts.callback)) {
3802
+ if (context.getCallPath(setterCall) !== binding.setterName)
3803
+ continue;
3804
+ diagnostics.push({
3805
+ node: callNameNode(setterCall),
3806
+ message: `${binding.setterName}() updates ${binding.valueName}, which is also in this effect's dependency table.`,
3807
+ help: "Guard the update so it converges, move it to the originating event, or derive the value during render."
3808
+ });
3809
+ }
3810
+ }
3811
+ continue;
3812
+ }
3813
+ const callback = effectCallbackWithoutDeps(context, call);
3814
+ if (!callback)
3573
3815
  continue;
3574
- const deps = parseDependencyRoots(parts.deps);
3575
3816
  for (const binding of stateBindingsFor(context, owner)) {
3576
- if (!deps.has(binding.valueName))
3577
- continue;
3578
- for (const setterCall of directTopLevelCalls(context, parts.callback)) {
3817
+ for (const setterCall of directTopLevelCalls(context, callback)) {
3579
3818
  if (context.getCallPath(setterCall) !== binding.setterName)
3580
3819
  continue;
3820
+ const argument = context.callArguments(setterCall)[0];
3821
+ if (!argument || !guaranteedRepeatedStateChange(argument, binding.valueName, context))
3822
+ continue;
3581
3823
  diagnostics.push({
3582
3824
  node: callNameNode(setterCall),
3583
- message: `${binding.setterName}() updates ${binding.valueName}, which is also in this effect's dependency table.`,
3584
- help: "Guard the update so it converges, move it to the originating event, or derive the value during render."
3825
+ message: `${binding.setterName}() unconditionally changes ${binding.valueName} in an effect that runs after every render.`,
3826
+ help: "Add a dependency table or convergence guard, move the update to the event that owns it, or derive the value during render."
3585
3827
  });
3586
3828
  }
3587
3829
  }
@@ -3597,8 +3839,8 @@ var noEffectWithFreshDeps = {
3597
3839
  run(context) {
3598
3840
  const diagnostics = [];
3599
3841
  for (const call of context.findCalls()) {
3600
- const path6 = context.resolveCallPath(context.getCallPath(call) ?? "");
3601
- if (!HOOKS_WITH_DEPS.has(path6))
3842
+ const path7 = context.resolveCallPath(context.getCallPath(call) ?? "");
3843
+ if (!HOOKS_WITH_DEPS.has(path7))
3602
3844
  continue;
3603
3845
  const owner = ownerForHook(context, call);
3604
3846
  const parts = callbackAndDeps(context, call);
@@ -3611,7 +3853,7 @@ var noEffectWithFreshDeps = {
3611
3853
  const label = dependency.type === "identifier" ? ` ${dependency.text}` : "";
3612
3854
  diagnostics.push({
3613
3855
  node: dependency,
3614
- message: `${path6} depends on${label || " an inline value"} that is a new ${kind} on every render.`,
3856
+ message: `${path7} depends on${label || " an inline value"} that is a new ${kind} on every render.`,
3615
3857
  help: "Move the value inside the hook callback and depend on its simple inputs, or intentionally stabilize the value with useMemo/useCallback when identity is part of the contract."
3616
3858
  });
3617
3859
  }
@@ -3627,8 +3869,8 @@ var noMutableInDeps = {
3627
3869
  run(context) {
3628
3870
  const diagnostics = [];
3629
3871
  for (const call of context.findCalls()) {
3630
- const path6 = context.resolveCallPath(context.getCallPath(call) ?? "");
3631
- if (!HOOKS_WITH_DEPS.has(path6))
3872
+ const path7 = context.resolveCallPath(context.getCallPath(call) ?? "");
3873
+ if (!HOOKS_WITH_DEPS.has(path7))
3632
3874
  continue;
3633
3875
  const owner = ownerForHook(context, call);
3634
3876
  const parts = callbackAndDeps(context, call);
@@ -3721,23 +3963,61 @@ var HOOK_ARGUMENTS = new Map([
3721
3963
  ["React.useImperativeHandle", { callbackIndex: 1, depsIndex: 2 }]
3722
3964
  ]);
3723
3965
  function parameterNames2(functionNode) {
3724
- const names = new Set;
3725
- const parameters = functionNode.childForFieldName("parameters");
3726
- if (!parameters)
3727
- return names;
3728
- for (const node of parameters.namedChildren) {
3729
- for (const candidate of descendants(node)) {
3730
- if (candidate.type === "identifier")
3731
- names.add(candidate.text);
3732
- }
3733
- }
3734
- return names;
3966
+ return new Set(parameterBindingNames(functionNode.childForFieldName("parameters")));
3735
3967
  }
3736
3968
  function* descendants(node) {
3737
3969
  yield node;
3738
3970
  for (const child of node.namedChildren)
3739
3971
  yield* descendants(child);
3740
3972
  }
3973
+ var TYPE_ONLY_NODES = new Set([
3974
+ "type_definition",
3975
+ "type_annotation",
3976
+ "type_parameters",
3977
+ "generic_type_list",
3978
+ "generic_type",
3979
+ "function_type",
3980
+ "optional_type",
3981
+ "object_type",
3982
+ "union_type",
3983
+ "intersection_type",
3984
+ "tuple_type",
3985
+ "singleton_type",
3986
+ "variadic_type",
3987
+ "variadic_type_pack"
3988
+ ]);
3989
+ function isTypeOnlyChild(parent, child) {
3990
+ if (TYPE_ONLY_NODES.has(child.type))
3991
+ return true;
3992
+ if (TYPE_ONLY_NODES.has(parent.type))
3993
+ return true;
3994
+ if (parent.type === "parameter") {
3995
+ return !sameNode(parent.namedChildren.find((candidate) => candidate.type === "identifier"), child);
3996
+ }
3997
+ if (parent.type === "cast_expression" || parent.type === "type_cast_expression") {
3998
+ return !sameNode(parent.namedChildren[0], child);
3999
+ }
4000
+ if (parent.type === "function_definition" || parent.type === "function_declaration") {
4001
+ const name = parent.childForFieldName("name");
4002
+ const parameters = parent.childForFieldName("parameters");
4003
+ const body = parent.childForFieldName("body");
4004
+ if (sameNode(child, name) || sameNode(child, parameters) || sameNode(child, body))
4005
+ return false;
4006
+ return true;
4007
+ }
4008
+ if (parent.type === "variable_list" && parent.parent?.parent?.type === "variable_declaration") {
4009
+ return true;
4010
+ }
4011
+ return false;
4012
+ }
4013
+ function* runtimeDescendants(node) {
4014
+ yield node;
4015
+ for (const child of node.namedChildren) {
4016
+ if (isTypeOnlyChild(node, child))
4017
+ continue;
4018
+ yield* runtimeDescendants(child);
4019
+ }
4020
+ }
3741
4021
  function directBlockBindingNames(block, beforeIndex) {
3742
4022
  const names = new Set;
3743
4023
  for (const child of block.namedChildren) {
@@ -3813,8 +4093,8 @@ function rootIdentifierNode(node, root) {
3813
4093
  }
3814
4094
  return null;
3815
4095
  }
3816
- function normalizeCapturedPath(path6, stableVariables, externallyMutableRoots) {
3817
- const normalized = normalizeExpressionText(path6);
4096
+ function normalizeCapturedPath(path7, stableVariables, externallyMutableRoots) {
4097
+ const normalized = normalizeExpressionText(path7);
3818
4098
  const root = rootIdentifier(normalized);
3819
4099
  if (!root)
3820
4100
  return normalized;
@@ -3831,7 +4111,7 @@ function normalizeCapturedPath(path6, stableVariables, externallyMutableRoots) {
3831
4111
  }
3832
4112
  function capturedDependencyPaths(callback, available, stableVariables, externallyMutableRoots) {
3833
4113
  const captured = new Set;
3834
- for (const node of descendants(callback)) {
4114
+ for (const node of runtimeDescendants(callback)) {
3835
4115
  if (node.type === "dot_index_expression" && outermostDotPath(node)) {
3836
4116
  const root = rootIdentifier(node.text);
3837
4117
  if (!root || !available.has(root) || GLOBALS.has(root) || stableVariables.has(root))
@@ -3900,8 +4180,81 @@ function expandLocalFunctionCaptures(captured, owner, available, stableVariables
3900
4180
  addCapture(capture);
3901
4181
  return result;
3902
4182
  }
4183
+ function dependencyCoveragePath(node) {
4184
+ if (node.type === "cast_expression" || node.type === "type_cast_expression" || node.type === "parenthesized_expression") {
4185
+ const expression = node.namedChildren[0];
4186
+ if (expression)
4187
+ return dependencyCoveragePath(expression);
4188
+ }
4189
+ if (node.type === "binary_expression") {
4190
+ const left = node.childForFieldName("left") ?? node.namedChildren[0];
4191
+ const right = node.childForFieldName("right") ?? node.namedChildren[1];
4192
+ const isOr = node.children.some((child) => child.type === "or");
4193
+ if (isOr && left && right?.type === "false")
4194
+ return dependencyCoveragePath(left);
4195
+ }
4196
+ return normalizeExpressionText(node.text);
4197
+ }
3903
4198
  function dependencyPaths(node) {
3904
- return new Set(dependencyExpressions(node).map((dependency) => normalizeExpressionText(dependency.text)));
4199
+ return new Set(dependencyExpressions(node).map(dependencyCoveragePath));
4200
+ }
4201
+ function transparentAliasPath(node) {
4202
+ if (node.type === "cast_expression" || node.type === "type_cast_expression" || node.type === "parenthesized_expression") {
4203
+ const expression = node.namedChildren[0];
4204
+ return expression ? transparentAliasPath(expression) : null;
4205
+ }
4206
+ if (node.type !== "identifier" && node.type !== "dot_index_expression")
4207
+ return null;
4208
+ return normalizeExpressionText(node.text);
4209
+ }
4210
+ function immutableTransparentAliases(owner, context) {
4211
+ const aliases = new Map;
4212
+ if (!owner.body)
4213
+ return aliases;
4214
+ const mutated = mutatedOwnerLocals(owner, context);
4215
+ for (const statement of owner.body.namedChildren) {
4216
+ if (statement.type !== "variable_declaration")
4217
+ continue;
4218
+ const names = declarationNames2(statement);
4219
+ const expressions = declarationExpressions3(statement);
4220
+ for (let index = 0;index < names.length; index += 1) {
4221
+ const name = names[index];
4222
+ const expression = expressions[index];
4223
+ if (!expression || mutated.has(name))
4224
+ continue;
4225
+ const source = transparentAliasPath(expression);
4226
+ if (!source || source === name)
4227
+ continue;
4228
+ const sourceRoot = rootIdentifier(source);
4229
+ if (!sourceRoot || mutated.has(sourceRoot))
4230
+ continue;
4231
+ aliases.set(name, source);
4232
+ }
4233
+ }
4234
+ return aliases;
4235
+ }
4236
+ function resolveAliasPath(path7, aliases) {
4237
+ let current = path7;
4238
+ const visiting = new Set;
4239
+ while (true) {
4240
+ const root = rootIdentifier(current);
4241
+ if (!root || visiting.has(root))
4242
+ return current;
4243
+ const source = aliases.get(root);
4244
+ if (!source)
4245
+ return current;
4246
+ visiting.add(root);
4247
+ current = `${source}${current.slice(root.length)}`;
4248
+ }
4249
+ }
4250
+ function dependencyPathsWithAliases(node, aliases) {
4251
+ const dependencies = dependencyPaths(node);
4252
+ for (const dependency of [...dependencies]) {
4253
+ const resolved = resolveAliasPath(dependency, aliases);
4254
+ if (resolved !== dependency)
4255
+ dependencies.add(resolved);
4256
+ }
4257
+ return dependencies;
3905
4258
  }
3906
4259
  var PURE_DERIVED_CALL = /^(?:math\.(?!random(?:seed)?$)[A-Za-z_][A-Za-z0-9_]*|string\.[A-Za-z_][A-Za-z0-9_]*|table\.(?:find|concat|clone|create|isfrozen)|(?:Color3|Vector2|Vector3|UDim|UDim2|CFrame|BrickColor|Rect|NumberRange|NumberSequence|ColorSequence|TweenInfo|Font|Ray|Region3|PhysicalProperties)\.[A-Za-z_][A-Za-z0-9_]*|[^:]+:(?:ToHSV|Lerp|Dot|Cross|FuzzyEq|Inverse|ToObjectSpace|ToWorldSpace))$/;
3907
4260
  function isPureDerivedExpression(node, context) {
@@ -3913,8 +4266,8 @@ function isPureDerivedExpression(node, context) {
3913
4266
  const rawPath = context.getCallPath(candidate);
3914
4267
  if (!rawPath)
3915
4268
  return false;
3916
- const path6 = normalizeExpressionText(context.resolveCallPath(rawPath));
3917
- if (!PURE_DERIVED_CALL.test(path6))
4269
+ const path7 = normalizeExpressionText(context.resolveCallPath(rawPath));
4270
+ if (!PURE_DERIVED_CALL.test(path7))
3918
4271
  return false;
3919
4272
  }
3920
4273
  return true;
@@ -3993,8 +4346,8 @@ function hasDynamicUnpackDependencies(node, context) {
3993
4346
  const rawPath = context.getCallPath(candidate);
3994
4347
  if (!rawPath)
3995
4348
  continue;
3996
- const path6 = normalizeExpressionText(context.resolveCallPath(rawPath));
3997
- if (path6 === "unpack" || path6 === "table.unpack")
4349
+ const path7 = normalizeExpressionText(context.resolveCallPath(rawPath));
4350
+ if (path7 === "unpack" || path7 === "table.unpack")
3998
4351
  return true;
3999
4352
  }
4000
4353
  }
@@ -4019,8 +4372,8 @@ function memoizedProducerDependencies(owner, context) {
4019
4372
  const rawPath = context.getCallPath(expression);
4020
4373
  if (!rawPath)
4021
4374
  continue;
4022
- const path6 = context.resolveCallPath(rawPath);
4023
- if (path6 !== "React.useMemo" && path6 !== "React.useCallback")
4375
+ const path7 = context.resolveCallPath(rawPath);
4376
+ if (path7 !== "React.useMemo" && path7 !== "React.useCallback")
4024
4377
  continue;
4025
4378
  const args = context.callArguments(expression);
4026
4379
  const deps = args[1];
@@ -4079,9 +4432,9 @@ function directCallsInExpression(node) {
4079
4432
  return calls;
4080
4433
  }
4081
4434
  function isCustomHookCall(call, context) {
4082
- const path6 = context.resolveCallPath(context.getCallPath(call) ?? "");
4083
- const final = path6.split(/[.:]/).at(-1) ?? "";
4084
- return !path6.startsWith("React.") && /^use[A-Z0-9_]/.test(final);
4435
+ const path7 = context.resolveCallPath(context.getCallPath(call) ?? "");
4436
+ const final = path7.split(/[.:]/).at(-1) ?? "";
4437
+ return !path7.startsWith("React.") && /^use[A-Z0-9_]/.test(final);
4085
4438
  }
4086
4439
  var HANDLE_VALUE_NAME = /(?:ref|binding|tween|spring|motion|animation|controller|handle|connection|signal)$/i;
4087
4440
  var HANDLE_HOOK_NAME = /(?:Tween|Spring|Binding|Motion|Animated|Animation|Sound)$/i;
@@ -4091,8 +4444,8 @@ function isCalledAsFunction(owner, name, context) {
4091
4444
  for (const call of context.walk(owner.body)) {
4092
4445
  if (call.type !== "function_call")
4093
4446
  continue;
4094
- const path6 = normalizeExpressionText(context.getCallPath(call) ?? "");
4095
- if (path6 === name)
4447
+ const path7 = normalizeExpressionText(context.getCallPath(call) ?? "");
4448
+ if (path7 === name)
4096
4449
  return true;
4097
4450
  }
4098
4451
  return false;
@@ -4122,8 +4475,8 @@ function customHookDependencyHints(owner, context) {
4122
4475
  continue;
4123
4476
  const directHook = calls.find((call) => isCustomHookCall(call, context));
4124
4477
  if (directHook && expression.type === "function_call") {
4125
- const path6 = context.resolveCallPath(context.getCallPath(directHook) ?? "");
4126
- const final = path6.split(/[.:]/).at(-1) ?? "";
4478
+ const path7 = context.resolveCallPath(context.getCallPath(directHook) ?? "");
4479
+ const final = path7.split(/[.:]/).at(-1) ?? "";
4127
4480
  const likelyHandle = HANDLE_VALUE_NAME.test(name) || HANDLE_HOOK_NAME.test(final) || isCalledAsFunction(owner, name, context);
4128
4481
  if (likelyHandle) {
4129
4482
  hints.memberHandleRoots.add(name);
@@ -4133,14 +4486,14 @@ function customHookDependencyHints(owner, context) {
4133
4486
  }
4134
4487
  if (expression.type === "table_constructor") {
4135
4488
  const meaningfulCalls = calls.filter((call) => {
4136
- const path6 = context.resolveCallPath(context.getCallPath(call) ?? "");
4137
- return !/^(?:Color3|Vector2|Vector3|UDim|UDim2|CFrame|TweenInfo|NumberSequence|ColorSequence)\./.test(path6);
4489
+ const path7 = context.resolveCallPath(context.getCallPath(call) ?? "");
4490
+ return !/^(?:Color3|Vector2|Vector3|UDim|UDim2|CFrame|TweenInfo|NumberSequence|ColorSequence)\./.test(path7);
4138
4491
  });
4139
4492
  const allHandleHooks = meaningfulCalls.length > 0 && meaningfulCalls.every((call) => {
4140
4493
  if (!isCustomHookCall(call, context))
4141
4494
  return false;
4142
- const path6 = context.resolveCallPath(context.getCallPath(call) ?? "");
4143
- const final = path6.split(/[.:]/).at(-1) ?? "";
4495
+ const path7 = context.resolveCallPath(context.getCallPath(call) ?? "");
4496
+ const final = path7.split(/[.:]/).at(-1) ?? "";
4144
4497
  return HANDLE_HOOK_NAME.test(final);
4145
4498
  });
4146
4499
  if (allHandleHooks)
@@ -4170,12 +4523,13 @@ var exhaustiveDeps = {
4170
4523
  const derivedDependenciesByOwner = new Map;
4171
4524
  const dependencyHintsByOwner = new Map;
4172
4525
  const memoizedProducersByOwner = new Map;
4526
+ const transparentAliasesByOwner = new Map;
4173
4527
  for (const call of context.findCalls()) {
4174
4528
  const rawPath = context.getCallPath(call);
4175
4529
  if (!rawPath)
4176
4530
  continue;
4177
- const path6 = context.resolveCallPath(rawPath);
4178
- const shape = HOOK_ARGUMENTS.get(path6);
4531
+ const path7 = context.resolveCallPath(rawPath);
4532
+ const shape = HOOK_ARGUMENTS.get(path7);
4179
4533
  if (!shape)
4180
4534
  continue;
4181
4535
  const args = context.callArguments(call);
@@ -4191,7 +4545,12 @@ var exhaustiveDeps = {
4191
4545
  const stableVariables = context.model.stableVariablesByFunction.get(ownerKey) ?? new Set;
4192
4546
  const externallyMutableRoots = context.model.externalMutableVariablesByFunction.get(ownerKey) ?? new Set;
4193
4547
  const functionExpandedCaptures = expandLocalFunctionCaptures(capturedDependencyPaths(callback, available, stableVariables, externallyMutableRoots), owner, available, stableVariables, externallyMutableRoots, context);
4194
- const dependencies = dependencyPaths(deps);
4548
+ let transparentAliases = transparentAliasesByOwner.get(ownerKey);
4549
+ if (!transparentAliases) {
4550
+ transparentAliases = immutableTransparentAliases(owner, context);
4551
+ transparentAliasesByOwner.set(ownerKey, transparentAliases);
4552
+ }
4553
+ const dependencies = dependencyPathsWithAliases(deps, transparentAliases);
4195
4554
  let derivedDependencies = derivedDependenciesByOwner.get(ownerKey);
4196
4555
  if (!derivedDependencies) {
4197
4556
  derivedDependencies = derivedLocalDependencies(owner, available, stableVariables, externallyMutableRoots, context);
@@ -4205,7 +4564,7 @@ var exhaustiveDeps = {
4205
4564
  diagnostics.push({
4206
4565
  node: tableStartNode(deps),
4207
4566
  severity: "suggestion",
4208
- message: `${path6} uses dynamic unpack(...) dependencies, so exhaustive dependency analysis is partial.`,
4567
+ message: `${path7} uses dynamic unpack(...) dependencies, so exhaustive dependency analysis is partial.`,
4209
4568
  help: "The analyzer cannot prove which values unpack(...) contributes. Review the explicit dependencies and the custom hook contract."
4210
4569
  });
4211
4570
  continue;
@@ -4226,7 +4585,7 @@ var exhaustiveDeps = {
4226
4585
  if (reactiveMissing.length > 0) {
4227
4586
  diagnostics.push({
4228
4587
  node: tableStartNode(deps),
4229
- message: `${path6} dependency table is missing ${reactiveMissing.join(", ")}.`,
4588
+ message: `${path7} dependency table is missing ${reactiveMissing.join(", ")}.`,
4230
4589
  help: `Add ${reactiveMissing.join(", ")} to the dependency table, or stop capturing ${reactiveMissing.length === 1 ? "that value" : "those values"} in the callback.`
4231
4590
  });
4232
4591
  }
@@ -4234,7 +4593,7 @@ var exhaustiveDeps = {
4234
4593
  diagnostics.push({
4235
4594
  node: tableStartNode(deps),
4236
4595
  severity: "suggestion",
4237
- message: `${path6} captures memoized value ${transitivelyCoveredMemoized.join(", ")} without listing ${transitivelyCoveredMemoized.length === 1 ? "it directly" : "them directly"}, but ${transitivelyCoveredMemoized.length === 1 ? "its memo dependencies are" : "their memo dependencies are"} already covered.`,
4596
+ message: `${path7} captures memoized value ${transitivelyCoveredMemoized.join(", ")} without listing ${transitivelyCoveredMemoized.length === 1 ? "it directly" : "them directly"}, but ${transitivelyCoveredMemoized.length === 1 ? "its memo dependencies are" : "their memo dependencies are"} already covered.`,
4238
4597
  help: "The producing memo dependencies are already covered. Listing the memoized value directly can make the dependency relationship clearer."
4239
4598
  });
4240
4599
  }
@@ -4242,7 +4601,7 @@ var exhaustiveDeps = {
4242
4601
  diagnostics.push({
4243
4602
  node: tableStartNode(deps),
4244
4603
  severity: "suggestion",
4245
- message: `${path6} captures custom-hook handle ${uncertainHandles.join(", ")} without listing ${uncertainHandles.length === 1 ? "it as a dependency" : "them as dependencies"}.`,
4604
+ message: `${path7} captures custom-hook handle ${uncertainHandles.join(", ")} without listing ${uncertainHandles.length === 1 ? "it as a dependency" : "them as dependencies"}.`,
4246
4605
  help: "The analyzer cannot prove this custom-hook return is stable. Check the hook contract before changing the dependency table."
4247
4606
  });
4248
4607
  }
@@ -4276,8 +4635,8 @@ function createElementCallsInLoop(context, loop) {
4276
4635
  for (const node of context.walk(loop)) {
4277
4636
  if (node.type !== "function_call")
4278
4637
  continue;
4279
- const path6 = context.resolveCallPath(context.getCallPath(node) ?? "");
4280
- if (path6 === "React.createElement")
4638
+ const path7 = context.resolveCallPath(context.getCallPath(node) ?? "");
4639
+ if (path7 === "React.createElement")
4281
4640
  result.push(node);
4282
4641
  }
4283
4642
  return result;
@@ -4333,8 +4692,8 @@ var noCreateContextInRender = {
4333
4692
  run(context) {
4334
4693
  const diagnostics = [];
4335
4694
  for (const call of context.findCalls()) {
4336
- const path6 = context.resolveCallPath(context.getCallPath(call) ?? "");
4337
- if (path6 !== "React.createContext")
4695
+ const path7 = context.resolveCallPath(context.getCallPath(call) ?? "");
4696
+ if (path7 !== "React.createContext")
4338
4697
  continue;
4339
4698
  const owner = context.nearestFunction(call);
4340
4699
  if (!owner || !owner.isComponent && !owner.isHook)
@@ -4370,8 +4729,8 @@ function isRenderedBy(context, component, name) {
4370
4729
  continue;
4371
4730
  if (context.nearestFunction(call) !== component)
4372
4731
  continue;
4373
- const path6 = context.resolveCallPath(context.getCallPath(call) ?? "");
4374
- if (path6 !== "React.createElement")
4732
+ const path7 = context.resolveCallPath(context.getCallPath(call) ?? "");
4733
+ if (path7 !== "React.createElement")
4375
4734
  continue;
4376
4735
  if (context.callArguments(call)[0]?.text.trim() === name)
4377
4736
  return true;
@@ -4415,63 +4774,6 @@ var noNestedComponentDefinition = {
4415
4774
  }
4416
4775
  };
4417
4776
 
4418
- // src/module-resolution.ts
4419
- import path6 from "path";
4420
- function normalizeRelative2(value) {
4421
- return value.split(path6.sep).join("/");
4422
- }
4423
- function moduleKeys(relativePath) {
4424
- let normalized = normalizeRelative2(relativePath).replace(/\.(?:lua|luau)$/i, "");
4425
- if (normalized.endsWith("/init"))
4426
- normalized = normalized.slice(0, -"/init".length);
4427
- const segments = normalized.split("/").filter(Boolean);
4428
- if (segments[0]?.toLowerCase() === "src")
4429
- segments.shift();
4430
- const keys = new Set;
4431
- for (let index = 0;index < segments.length; index += 1) {
4432
- const suffix = segments.slice(index).join(".").toLowerCase();
4433
- if (suffix)
4434
- keys.add(suffix);
4435
- }
4436
- return [...keys];
4437
- }
4438
- function normalizeRequireTarget(text) {
4439
- return (text.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? []).join(".").toLowerCase();
4440
- }
4441
- function buildUniqueFeatureAliases(modules, featureByModuleId) {
4442
- const owners = new Map;
4443
- for (const module of modules) {
4444
- for (const key of module.keys) {
4445
- const ids = owners.get(key) ?? new Set;
4446
- ids.add(module.id);
4447
- owners.set(key, ids);
4448
- }
4449
- }
4450
- const aliases = new Map;
4451
- for (const module of modules) {
4452
- const value = featureByModuleId.get(module.id);
4453
- if (value === undefined)
4454
- continue;
4455
- for (const key of module.keys) {
4456
- const ids = owners.get(key);
4457
- if (ids?.size === 1 && ids.has(module.id))
4458
- aliases.set(key, value);
4459
- }
4460
- }
4461
- return aliases;
4462
- }
4463
- function resolveModuleReference(target, aliases) {
4464
- let candidate = target;
4465
- while (true) {
4466
- if (aliases.has(candidate))
4467
- return aliases.get(candidate);
4468
- const separator = candidate.indexOf(".");
4469
- if (separator < 0)
4470
- return null;
4471
- candidate = candidate.slice(separator + 1);
4472
- }
4473
- }
4474
-
4475
4777
  // src/rules/parameter-mutations.ts
4476
4778
  var analysisCache = new WeakMap;
4477
4779
  var EMPTY_INDEXES = new Set;
@@ -7194,6 +7496,20 @@ function staticIterationImports(context) {
7194
7496
  }
7195
7497
  return result;
7196
7498
  }
7499
+ function nonReactHookImports(context) {
7500
+ const result = new Set;
7501
+ for (const node of context.walk(context.root)) {
7502
+ if (node.type !== "variable_declaration")
7503
+ continue;
7504
+ const match = node.text.match(/^\s*local\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*require\s*\((.*?)\)\s*$/s);
7505
+ if (!match || !/^use[A-Z0-9_]/.test(match[1]))
7506
+ continue;
7507
+ const kind = resolveModuleReference(normalizeRequireTarget(match[2]), context.project.reactHookModules);
7508
+ if (kind === false)
7509
+ result.add(match[1]);
7510
+ }
7511
+ return result;
7512
+ }
7197
7513
  function conditionalHookModeImports(context) {
7198
7514
  const result = new Map;
7199
7515
  for (const node of context.walk(context.root)) {
@@ -7241,30 +7557,12 @@ function controlledParameterIndex(controlFlow, fn, summary) {
7241
7557
  }
7242
7558
  return null;
7243
7559
  }
7244
- function isStableModeArgument(node, context) {
7245
- if (!node)
7246
- return true;
7247
- const text = node.text.trim();
7248
- if (["true", "false", "nil"].includes(text))
7249
- return true;
7250
- if ([
7251
- "number",
7252
- "string",
7253
- "string_content",
7254
- "table_constructor",
7255
- "function_definition"
7256
- ].includes(node.type))
7257
- return true;
7258
- if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(text) && context.model.stableVariables.has(text))
7259
- return true;
7260
- return false;
7261
- }
7262
7560
  function dynamicHookModeFixPreview(hookName, argumentText) {
7263
7561
  return {
7264
7562
  kind: "pattern",
7265
7563
  before: `local value = ${hookName}(${argumentText})`,
7266
- after: `local value = ${hookName}(true) -- or false; keep this mode fixed for this call site`,
7267
- note: "A custom hook may choose different internal hooks only when this mode is stable for the lifetime of the component instance."
7564
+ after: `local value = ${hookName}(${argumentText}) -- keep the hook-topology mode stable for this instance`,
7565
+ note: "A custom hook may choose different internal hooks only when the value controlling that choice stays stable for the lifetime of the component instance."
7268
7566
  };
7269
7567
  }
7270
7568
  function unwrapIteratorExpression(text) {
@@ -7284,6 +7582,17 @@ function declarationExpressions4(node) {
7284
7582
  function isEmptyTable(node) {
7285
7583
  return Boolean(node?.type === "table_constructor" && node.namedChildren.every((child) => child.type !== "field"));
7286
7584
  }
7585
+ function tableConstructorHasStableArity(node) {
7586
+ if (node.type !== "table_constructor")
7587
+ return false;
7588
+ const fields = node.namedChildren.filter((child) => child.type === "field");
7589
+ if (fields.length === 0)
7590
+ return true;
7591
+ if (fields.some((field) => [...field.namedChildren].some((child) => child.type === "vararg_expression")))
7592
+ return false;
7593
+ const last = fields.at(-1);
7594
+ return !last?.namedChildren.some((child) => child.type === "function_call");
7595
+ }
7287
7596
  function stableShapeVariablesByFunction(context) {
7288
7597
  const stable = new Map;
7289
7598
  const functionsByName = new Map;
@@ -7310,7 +7619,7 @@ function stableShapeVariablesByFunction(context) {
7310
7619
  const expression = expressions[index] ?? expressions[0];
7311
7620
  if (!expression)
7312
7621
  continue;
7313
- if (expression.type === "table_constructor") {
7622
+ if (expression.type === "table_constructor" && tableConstructorHasStableArity(expression)) {
7314
7623
  namesFor(owner).add(names[index]);
7315
7624
  continue;
7316
7625
  }
@@ -7337,7 +7646,7 @@ function stableShapeVariablesByFunction(context) {
7337
7646
  if (!expression)
7338
7647
  return false;
7339
7648
  if (expression.type === "table_constructor")
7340
- return true;
7649
+ return tableConstructorHasStableArity(expression);
7341
7650
  const text = expression.text.trim();
7342
7651
  if (!owner || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(text))
7343
7652
  return false;
@@ -7390,6 +7699,187 @@ function stableShapeVariablesByFunction(context) {
7390
7699
  }
7391
7700
  return stable;
7392
7701
  }
7702
+ function moduleInvariantVariables(context) {
7703
+ const declarations = new Map;
7704
+ const reassigned = new Set;
7705
+ for (const node of context.walk(context.root)) {
7706
+ if (node.type === "variable_declaration" && !context.nearestFunction(node)) {
7707
+ for (const name of declarationNames2(node))
7708
+ declarations.set(name, node);
7709
+ continue;
7710
+ }
7711
+ if (node.type !== "assignment_statement" || node.parent?.type === "variable_declaration")
7712
+ continue;
7713
+ const variableList = node.namedChildren.find((child) => child.type === "variable_list");
7714
+ for (const target of variableList?.namedChildren ?? []) {
7715
+ if (target.type === "identifier")
7716
+ reassigned.add(target.text);
7717
+ }
7718
+ }
7719
+ return new Set([...declarations.keys()].filter((name) => !reassigned.has(name)));
7720
+ }
7721
+ function simpleConditionRoot(controlFlow) {
7722
+ if (controlFlow.type !== "if_statement" && controlFlow.type !== "if_expression")
7723
+ return null;
7724
+ const condition = controlFlow.namedChildren[0];
7725
+ if (!condition)
7726
+ return null;
7727
+ const text = condition.text.trim();
7728
+ const match = text.match(/^(?:not\s+)?([A-Za-z_][A-Za-z0-9_]*)(?:\s*(?:==|~=)\s*(?:true|false|nil))?$/);
7729
+ return match?.[1] ?? null;
7730
+ }
7731
+ function conditionIsProvablyInvariant(controlFlow, fn, context, moduleInvariants) {
7732
+ const condition = controlFlow.namedChildren[0];
7733
+ if (!condition)
7734
+ return false;
7735
+ if (["true", "false", "nil", "number", "string"].includes(condition.type))
7736
+ return true;
7737
+ const root = simpleConditionRoot(controlFlow);
7738
+ if (!root)
7739
+ return false;
7740
+ if (moduleInvariants.has(root))
7741
+ return true;
7742
+ return Boolean(context.model.stableVariablesByFunction.get(nodeKey(fn.node))?.has(root));
7743
+ }
7744
+ function branchHookSequence(branch, fn, context) {
7745
+ const hooks = [];
7746
+ for (const node of context.walk(branch)) {
7747
+ if (node.type !== "function_call")
7748
+ continue;
7749
+ if (context.nearestFunction(node) !== fn)
7750
+ continue;
7751
+ const rawPath = context.getCallPath(node);
7752
+ if (!rawPath)
7753
+ continue;
7754
+ const path7 = context.resolveCallPath(rawPath);
7755
+ if (!isHookPath(path7))
7756
+ continue;
7757
+ const nestedControl = findAncestorBetween(node, branch, (ancestor) => CONDITIONAL_TYPES.has(ancestor.type));
7758
+ if (nestedControl)
7759
+ return null;
7760
+ hooks.push(path7);
7761
+ }
7762
+ return hooks;
7763
+ }
7764
+ function ifBranchesHaveEquivalentBuiltInTopology(controlFlow, fn, context) {
7765
+ if (controlFlow.type !== "if_statement")
7766
+ return false;
7767
+ const thenBlock = controlFlow.namedChildren.find((child) => child.type === "block");
7768
+ const elseStatement = controlFlow.namedChildren.find((child) => child.type === "else_statement");
7769
+ const elseBlock = elseStatement?.namedChildren.find((child) => child.type === "block");
7770
+ if (!thenBlock || !elseBlock)
7771
+ return false;
7772
+ const thenHooks = branchHookSequence(thenBlock, fn, context);
7773
+ const elseHooks = branchHookSequence(elseBlock, fn, context);
7774
+ if (!thenHooks || !elseHooks || thenHooks.length !== elseHooks.length)
7775
+ return false;
7776
+ if (!thenHooks.every((path7, index) => path7 === elseHooks[index]))
7777
+ return false;
7778
+ return thenHooks.every((path7) => path7.startsWith("React."));
7779
+ }
7780
+ function stateModeStability(name, owner, context) {
7781
+ const binding = context.model.stateBindings.find((candidate) => candidate.owner === owner && candidate.valueName === name);
7782
+ if (!binding || !owner.body)
7783
+ return null;
7784
+ const initializerText = binding.initializer?.text.trim() ?? "nil";
7785
+ const stableLiteral = /^(?:true|false|nil|-?\d+(?:\.\d+)?|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/;
7786
+ let sawIndirectSetterReference = false;
7787
+ for (const node of context.walk(owner.body)) {
7788
+ if (node.type === "function_call" && context.getCallPath(node) === binding.setterName) {
7789
+ const argument = context.callArguments(node)[0];
7790
+ if (!argument || !stableLiteral.test(initializerText) || argument.text.trim() !== initializerText)
7791
+ return "unstable";
7792
+ continue;
7793
+ }
7794
+ if (node.type !== "identifier" || node.text !== binding.setterName)
7795
+ continue;
7796
+ if (node.startIndex >= binding.declaration.startIndex && node.endIndex <= binding.declaration.endIndex)
7797
+ continue;
7798
+ if (node.parent?.type === "function_call")
7799
+ continue;
7800
+ sawIndirectSetterReference = true;
7801
+ }
7802
+ if (sawIndirectSetterReference)
7803
+ return "unknown";
7804
+ return "stable";
7805
+ }
7806
+ function localAliasExpression(name, owner, context) {
7807
+ if (!owner.body)
7808
+ return null;
7809
+ let result = null;
7810
+ for (const node of context.walk(owner.body)) {
7811
+ if (node.type !== "variable_declaration" || context.nearestFunction(node) !== owner)
7812
+ continue;
7813
+ const names = declarationNames2(node);
7814
+ const index = names.indexOf(name);
7815
+ if (index < 0)
7816
+ continue;
7817
+ const expressions = declarationExpressions4(node);
7818
+ const expression = expressions[index] ?? expressions[0] ?? null;
7819
+ if (result)
7820
+ return null;
7821
+ result = expression;
7822
+ }
7823
+ return result;
7824
+ }
7825
+ function modeValueStability(node, owner, context, moduleInvariants, depth = 0) {
7826
+ if (!node)
7827
+ return "stable";
7828
+ if (depth > 4)
7829
+ return "unknown";
7830
+ const text = node.text.trim();
7831
+ if (["true", "false", "nil"].includes(text))
7832
+ return "stable";
7833
+ if (["number", "string", "string_content", "function_definition"].includes(node.type))
7834
+ return "stable";
7835
+ const root = text.match(/^([A-Za-z_][A-Za-z0-9_]*)/)?.[1];
7836
+ if (!root)
7837
+ return "unknown";
7838
+ const stateStability = stateModeStability(root, owner, context);
7839
+ if (stateStability)
7840
+ return stateStability;
7841
+ if (moduleInvariants.has(root))
7842
+ return "stable";
7843
+ if (context.model.stableVariablesByFunction.get(nodeKey(owner.node))?.has(root))
7844
+ return "stable";
7845
+ if (text === root) {
7846
+ const alias = localAliasExpression(root, owner, context);
7847
+ if (alias && alias.text.trim() !== root)
7848
+ return modeValueStability(alias, owner, context, moduleInvariants, depth + 1);
7849
+ }
7850
+ return "unknown";
7851
+ }
7852
+ function tableFieldValue(table, accessPath) {
7853
+ if (!table || accessPath === "")
7854
+ return table;
7855
+ if (table.type !== "table_constructor")
7856
+ return;
7857
+ let current = table;
7858
+ for (const part of accessPath.split(".")) {
7859
+ if (!current || current.type !== "table_constructor")
7860
+ return;
7861
+ const field = current.namedChildren.find((child) => {
7862
+ if (child.type !== "field")
7863
+ return false;
7864
+ return child.namedChildren[0]?.type === "identifier" && child.namedChildren[0]?.text === part;
7865
+ });
7866
+ if (!field)
7867
+ return;
7868
+ current = field.namedChildren[1];
7869
+ }
7870
+ return current;
7871
+ }
7872
+ function controlledAccessPaths(summary, parameterIndex) {
7873
+ const paths = new Set;
7874
+ for (const [conditionName, index] of Object.entries(summary.conditionVariables)) {
7875
+ if (index !== parameterIndex)
7876
+ continue;
7877
+ paths.add(summary.conditionAccessPaths?.[conditionName] ?? "");
7878
+ }
7879
+ if (paths.size === 0)
7880
+ paths.add("");
7881
+ return [...paths];
7882
+ }
7393
7883
  function loopIterationIsProvablyStable(loop, imports, owner, stableShapes) {
7394
7884
  const numeric = loop.namedChildren.find((child) => child.type === "for_numeric_clause");
7395
7885
  if (numeric) {
@@ -7451,9 +7941,9 @@ function impossibleNilGuardReturn(node, fn, parameterTypes) {
7451
7941
  }
7452
7942
  return false;
7453
7943
  }
7454
- function hasReachableReturnBefore(call, fn, context, cache) {
7944
+ function reachableReturnsBefore(call, fn, context, cache) {
7455
7945
  if (!fn.body)
7456
- return false;
7946
+ return [];
7457
7947
  const key = nodeKey(fn.node);
7458
7948
  let returns = cache.get(key);
7459
7949
  if (!returns) {
@@ -7461,7 +7951,21 @@ function hasReachableReturnBefore(call, fn, context, cache) {
7461
7951
  returns = [...context.walk(fn.body)].filter((node) => node.type === "return_statement" && !isNestedInsideFunction(node, fn.node) && !impossibleNilGuardReturn(node, fn, parameterTypes));
7462
7952
  cache.set(key, returns);
7463
7953
  }
7464
- return returns.some((node) => node.endIndex <= call.startIndex);
7954
+ return returns.filter((node) => node.endIndex <= call.startIndex);
7955
+ }
7956
+ function returnIsControlledByStableTopologyMode(node, fn, summary, context, moduleInvariants) {
7957
+ let current = node.parent;
7958
+ while (current && current !== fn.node) {
7959
+ if (current.type === "if_statement" || current.type === "if_expression") {
7960
+ if (summary && controlledParameterIndex(current, fn, summary) !== null)
7961
+ return true;
7962
+ if (conditionIsProvablyInvariant(current, fn, context, moduleInvariants))
7963
+ return true;
7964
+ return false;
7965
+ }
7966
+ current = current.parent;
7967
+ }
7968
+ return false;
7465
7969
  }
7466
7970
  function conditionalFixPreview(kind, hookName) {
7467
7971
  if (kind === "for statement") {
@@ -7533,15 +8037,21 @@ var rulesOfHooks = {
7533
8037
  description: "Hooks must run in the same order on every render of a React component or custom hook.",
7534
8038
  run(context) {
7535
8039
  const diagnostics = [];
8040
+ if (!context.model.isReactFile)
8041
+ return diagnostics;
7536
8042
  const staticImports = staticIterationImports(context);
7537
8043
  const stableShapes = stableShapeVariablesByFunction(context);
8044
+ const moduleInvariants = moduleInvariantVariables(context);
7538
8045
  const modeImports = conditionalHookModeImports(context);
8046
+ const ignoredHookImports = nonReactHookImports(context);
7539
8047
  const currentModeSummary = currentConditionalHookMode(context);
7540
8048
  const reachableReturns = new Map;
7541
8049
  for (const call of context.findCalls()) {
7542
8050
  const rawPath = context.getCallPath(call);
7543
8051
  if (!rawPath)
7544
8052
  continue;
8053
+ if (ignoredHookImports.has(rawPath))
8054
+ continue;
7545
8055
  const path7 = context.resolveCallPath(rawPath);
7546
8056
  if (!isHookPath(path7))
7547
8057
  continue;
@@ -7570,25 +8080,36 @@ end`,
7570
8080
  const importedMode = modeImports.get(rawPath);
7571
8081
  if (importedMode) {
7572
8082
  const args = context.callArguments(call);
7573
- const dynamicIndexes = importedMode.parameterIndexes.filter((index) => !isStableModeArgument(args[index], context));
7574
- if (dynamicIndexes.length > 0) {
7575
- const firstIndex = dynamicIndexes[0];
7576
- const argument = args[firstIndex];
8083
+ const unstableControls = importedMode.parameterIndexes.flatMap((index) => controlledAccessPaths(importedMode, index).map((accessPath) => {
8084
+ const argument = args[index];
8085
+ const value = tableFieldValue(argument, accessPath);
8086
+ const stability = accessPath !== "" ? argument?.type === "table_constructor" ? value ? modeValueStability(value, fn, context, moduleInvariants) : "stable" : "unknown" : modeValueStability(argument, fn, context, moduleInvariants);
8087
+ return { index, accessPath, argument, value, stability };
8088
+ }).filter((control) => control.stability === "unstable"));
8089
+ if (unstableControls.length > 0) {
8090
+ const first = unstableControls[0];
8091
+ const firstIndex = first.index;
8092
+ const argument = first.argument;
7577
8093
  const parameterOffset = importedMode.parameterIndexes.indexOf(firstIndex);
7578
- const parameterName = importedMode.parameterNames[parameterOffset] ?? `argument ${firstIndex + 1}`;
7579
- const argumentText = argument?.text.trim() || "<omitted>";
8094
+ const baseParameterName = importedMode.parameterNames[parameterOffset] ?? `argument ${firstIndex + 1}`;
8095
+ const parameterName = first.accessPath ? `${baseParameterName}.${first.accessPath}` : baseParameterName;
8096
+ const argumentText = first.value?.text.trim() || argument?.text.trim() || "<omitted>";
7580
8097
  diagnostics.push({
7581
- node: argument ?? callNameNode(call),
7582
- highlights: dynamicIndexes.map((index) => args[index]).filter((node) => Boolean(node)),
7583
- message: `Hook ${rawPath} receives render-varying hook mode ${parameterName} from ${argumentText}; if it changes between renders, the custom hook can execute a different hook sequence.`,
7584
- help: `Pass a stable mode at this call site, or refactor ${rawPath} so it always calls the same hooks regardless of ${parameterName}.`,
8098
+ node: first.value ?? argument ?? callNameNode(call),
8099
+ highlights: unstableControls.map((control) => control.value ?? control.argument).filter((node) => Boolean(node)),
8100
+ message: `Hook ${rawPath} receives hook-topology mode ${parameterName} from ${argumentText}, which is updated between renders and can make the custom hook execute a different hook sequence.`,
8101
+ help: `Keep ${parameterName} stable for this hook instance, or refactor ${rawPath} so it always calls the same hooks regardless of that mode.`,
7585
8102
  fixPreview: dynamicHookModeFixPreview(rawPath, argumentText)
7586
8103
  });
7587
8104
  }
7588
8105
  }
7589
8106
  const controlFlow = findAncestorBetween(call, fn.node, (node) => CONDITIONAL_TYPES.has(node.type));
7590
8107
  if (controlFlow) {
7591
- if (currentModeSummary && currentModeSummary.knownCallSites > 0 && controlledParameterIndex(controlFlow, fn, currentModeSummary) !== null)
8108
+ if (currentModeSummary && controlledParameterIndex(controlFlow, fn, currentModeSummary) !== null)
8109
+ continue;
8110
+ if (conditionIsProvablyInvariant(controlFlow, fn, context, moduleInvariants))
8111
+ continue;
8112
+ if (ifBranchesHaveEquivalentBuiltInTopology(controlFlow, fn, context))
7592
8113
  continue;
7593
8114
  if (controlFlow.type === "for_statement" && loopIterationIsProvablyStable(controlFlow, staticImports, fn, stableShapes))
7594
8115
  continue;
@@ -7602,7 +8123,8 @@ end`,
7602
8123
  });
7603
8124
  continue;
7604
8125
  }
7605
- if (hasReachableReturnBefore(call, fn, context, reachableReturns)) {
8126
+ const earlierReturns = reachableReturnsBefore(call, fn, context, reachableReturns);
8127
+ if (earlierReturns.length > 0 && !earlierReturns.every((node) => returnIsControlledByStableTopologyMode(node, fn, currentModeSummary, context, moduleInvariants))) {
7606
8128
  diagnostics.push({
7607
8129
  node: callNameNode(call),
7608
8130
  message: `Hook ${path7} may run after an earlier reachable return, so some renders can execute fewer hooks.`,
@@ -8075,7 +8597,7 @@ function toDiagnostic(file, ruleId, category, severity, input) {
8075
8597
  }
8076
8598
  async function analyzeReactFile(input, tree) {
8077
8599
  const parsedTree = tree ?? await parseLuau(input.source);
8078
- const model = buildReactModel(parsedTree.rootNode);
8600
+ const model = buildReactModel(parsedTree.rootNode, input.project);
8079
8601
  if (!input.forceScan && !model.isReactFile) {
8080
8602
  return { relativePath: input.relativePath, isReactFile: false, scanned: false, diagnostics: [] };
8081
8603
  }
@@ -8447,37 +8969,81 @@ function parameterName(parameter) {
8447
8969
  function branchContainsHookCall(text) {
8448
8970
  return /\b(?:[A-Za-z_][A-Za-z0-9_]*[.:])?use[A-Z0-9_][A-Za-z0-9_]*\s*\(/.test(text);
8449
8971
  }
8972
+ var BUILT_IN_REACT_HOOK_NAMES = new Set([
8973
+ "useState",
8974
+ "useEffect",
8975
+ "useLayoutEffect",
8976
+ "useMemo",
8977
+ "useCallback",
8978
+ "useContext",
8979
+ "useReducer",
8980
+ "useRef",
8981
+ "useImperativeHandle",
8982
+ "useBinding"
8983
+ ]);
8984
+ function simpleBranchHookPaths(text) {
8985
+ if (/\b(?:if|for|while|repeat|function)\b/.test(text))
8986
+ return null;
8987
+ return [...text.matchAll(/\b(React\.(use[A-Z0-9_][A-Za-z0-9_]*))\s*\(/g)].map((match) => match[2]);
8988
+ }
8989
+ function simpleIfHasEquivalentBuiltInTopology(source, ifStart) {
8990
+ if (ifStart === undefined)
8991
+ return false;
8992
+ const statement = source.slice(ifStart).match(/^if\b[\s\S]*?\bthen\b([\s\S]*?)\belse\b([\s\S]*?)\bend\b/);
8993
+ if (!statement)
8994
+ return false;
8995
+ const left = simpleBranchHookPaths(statement[1] ?? "");
8996
+ const right = simpleBranchHookPaths(statement[2] ?? "");
8997
+ if (!left || !right || left.length === 0 || left.length !== right.length)
8998
+ return false;
8999
+ return left.every((name, index) => name === right[index] && BUILT_IN_REACT_HOOK_NAMES.has(name));
9000
+ }
8450
9001
  function conditionAliasesForParameter(source, parameter) {
8451
- const aliases = new Set([parameter]);
9002
+ const aliases = new Map([[parameter, ""]]);
8452
9003
  const escaped = parameter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9004
+ for (const match of source.matchAll(new RegExp(`\\b${escaped}((?:\\.[A-Za-z_][A-Za-z0-9_]*)+)`, "g"))) {
9005
+ aliases.set(`${parameter}${match[1]}`, match[1].slice(1));
9006
+ }
8453
9007
  const directPatterns = [
8454
- new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*${escaped}\\s*$`, "gm"),
8455
- new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*not\\s+${escaped}\\s*$`, "gm"),
8456
- new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*${escaped}\\s*(?:==|~=)\\s*(?:true|false|nil)\\s*$`, "gm"),
8457
- new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*(?:true|false|nil)\\s*(?:==|~=)\\s*${escaped}\\s*$`, "gm"),
8458
- new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*if\\s+${escaped}\\s*(?:==|~=)\\s*nil\\s+then\\s+(?:true|false)\\s+else\\s+${escaped}\\s*$`, "gm"),
8459
- new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*if\\s+${escaped}\\s+then\\s+(?:true|false)\\s+else\\s+(?:true|false)\\s*$`, "gm")
9008
+ new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*(${escaped}(?:\\.[A-Za-z_][A-Za-z0-9_]*)*)\\s*$`, "gm"),
9009
+ new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*not\\s+(${escaped}(?:\\.[A-Za-z_][A-Za-z0-9_]*)*)\\s*$`, "gm"),
9010
+ new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*(${escaped}(?:\\.[A-Za-z_][A-Za-z0-9_]*)*)\\s*(?:==|~=)\\s*(?:true|false|nil)\\s*$`, "gm"),
9011
+ new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*(?:true|false|nil)\\s*(?:==|~=)\\s*(${escaped}(?:\\.[A-Za-z_][A-Za-z0-9_]*)*)\\s*$`, "gm")
8460
9012
  ];
8461
9013
  for (const pattern of directPatterns) {
8462
- for (const match of source.matchAll(pattern))
8463
- aliases.add(match[1]);
9014
+ for (const match of source.matchAll(pattern)) {
9015
+ const sourcePath = match[2];
9016
+ const accessPath = sourcePath === parameter ? "" : sourcePath.slice(parameter.length + 1);
9017
+ aliases.set(match[1], accessPath);
9018
+ }
9019
+ }
9020
+ for (const match of source.matchAll(new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*if\\s+(${escaped}(?:\\.[A-Za-z_][A-Za-z0-9_]*)*)\\s*(?:==|~=)\\s*nil\\s+then\\s+(?:true|false)\\s+else\\s+\\2\\s*$`, "gm"))) {
9021
+ const sourcePath = match[2];
9022
+ aliases.set(match[1], sourcePath === parameter ? "" : sourcePath.slice(parameter.length + 1));
9023
+ }
9024
+ for (const match of source.matchAll(new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*if\\s+(${escaped}(?:\\.[A-Za-z_][A-Za-z0-9_]*)*)\\s+then\\s+(?:true|false)\\s+else\\s+(?:true|false)\\s*$`, "gm"))) {
9025
+ const sourcePath = match[2];
9026
+ aliases.set(match[1], sourcePath === parameter ? "" : sourcePath.slice(parameter.length + 1));
8464
9027
  }
8465
- return [...aliases];
9028
+ return aliases;
8466
9029
  }
8467
9030
  function findConditionalHookMode(source) {
8468
9031
  const exported = exportedFunctionParameters(source);
8469
9032
  if (!exported)
8470
9033
  return null;
9034
+ if (exported.name && !/^use[A-Z0-9_]/.test(exported.name))
9035
+ return null;
8471
9036
  const parameterNames3 = exported.parameters.map(parameterName);
8472
9037
  const controlledIndexes = [];
8473
9038
  const controlledNames = [];
8474
9039
  const conditionVariables = {};
9040
+ const conditionAccessPaths = {};
8475
9041
  for (const [index, name] of parameterNames3.entries()) {
8476
9042
  if (!name)
8477
9043
  continue;
8478
9044
  const aliases = conditionAliasesForParameter(source, name);
8479
9045
  let controlsHook = false;
8480
- for (const alias of aliases) {
9046
+ for (const [alias, accessPath] of aliases) {
8481
9047
  const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8482
9048
  const patterns = [
8483
9049
  new RegExp(`\\bif\\s+${escaped}\\s+then([\\s\\S]*?)(?=\\belse\\b|\\bend\\b)`, "g"),
@@ -8489,8 +9055,11 @@ function findConditionalHookMode(source) {
8489
9055
  for (const match of source.matchAll(pattern)) {
8490
9056
  if (!branchContainsHookCall(match[1] ?? ""))
8491
9057
  continue;
9058
+ if (simpleIfHasEquivalentBuiltInTopology(source, match.index))
9059
+ continue;
8492
9060
  controlsHook = true;
8493
9061
  conditionVariables[alias] = index;
9062
+ conditionAccessPaths[alias] = accessPath;
8494
9063
  break;
8495
9064
  }
8496
9065
  if (controlsHook)
@@ -8509,6 +9078,7 @@ function findConditionalHookMode(source) {
8509
9078
  parameterIndexes: controlledIndexes,
8510
9079
  parameterNames: controlledNames,
8511
9080
  conditionVariables,
9081
+ conditionAccessPaths,
8512
9082
  knownCallSites: 0,
8513
9083
  dynamicCallSites: 0
8514
9084
  };
@@ -9159,6 +9729,63 @@ function findBindingCompatibleComponentProps(source) {
9159
9729
  }
9160
9730
  return compatible.size > 0 ? compatible : null;
9161
9731
  }
9732
+ function exportedUseFunctionName(source) {
9733
+ const name = exportedFunctionName(source);
9734
+ return name && /^use[A-Z0-9_]/.test(name) ? name : null;
9735
+ }
9736
+ function directReactHookUsage(source) {
9737
+ const namespaces = new Set(["React"]);
9738
+ const requirePattern = /\blocal\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*require\s*\((.*?)\)/gs;
9739
+ for (const match of source.matchAll(requirePattern)) {
9740
+ const target = normalizeRequireTarget(match[2]);
9741
+ if (target.split(".").includes("react"))
9742
+ namespaces.add(match[1]);
9743
+ }
9744
+ for (const namespace of namespaces) {
9745
+ const escaped = namespace.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9746
+ if (new RegExp(`\\b${escaped}\\.use[A-Z0-9_][A-Za-z0-9_]*\\b`).test(source))
9747
+ return true;
9748
+ }
9749
+ return false;
9750
+ }
9751
+ function calledRequiredModules(source) {
9752
+ const result = [];
9753
+ const requirePattern = /\blocal\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*require\s*\((.*?)\)/gs;
9754
+ for (const match of source.matchAll(requirePattern)) {
9755
+ const localName = match[1];
9756
+ const escaped = localName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9757
+ if (!new RegExp(`\\b${escaped}\\s*\\(`).test(source.slice(match.index + match[0].length)))
9758
+ continue;
9759
+ result.push({ localName, target: normalizeRequireTarget(match[2]) });
9760
+ }
9761
+ return result;
9762
+ }
9763
+ function findReactHookModules(records) {
9764
+ const hookRecords = records.filter((record) => exportedUseFunctionName(record.source));
9765
+ const provenReact = new Map;
9766
+ for (const record of hookRecords) {
9767
+ if (directReactHookUsage(record.source))
9768
+ provenReact.set(record.id, true);
9769
+ }
9770
+ let changed = true;
9771
+ while (changed) {
9772
+ changed = false;
9773
+ const aliases = buildUniqueFeatureAliases(records, provenReact);
9774
+ for (const record of hookRecords) {
9775
+ if (provenReact.has(record.id))
9776
+ continue;
9777
+ const callsReactHookModule = calledRequiredModules(record.source).some(({ target }) => resolveModuleReference(target, aliases) === true);
9778
+ if (!callsReactHookModule)
9779
+ continue;
9780
+ provenReact.set(record.id, true);
9781
+ changed = true;
9782
+ }
9783
+ }
9784
+ const result = new Map;
9785
+ for (const record of hookRecords)
9786
+ result.set(record.id, provenReact.has(record.id));
9787
+ return result;
9788
+ }
9162
9789
  function findStaticIterationTables(source) {
9163
9790
  const match = /^return\s*\{/m.exec(source);
9164
9791
  if (!match || match.index === undefined)
@@ -9234,11 +9861,13 @@ function buildProjectModel(root, candidates, options = {}) {
9234
9861
  parameterIndexes: [...analysis.conditionalHookMode.parameterIndexes],
9235
9862
  parameterNames: [...analysis.conditionalHookMode.parameterNames],
9236
9863
  conditionVariables: { ...analysis.conditionalHookMode.conditionVariables },
9864
+ conditionAccessPaths: { ...analysis.conditionalHookMode.conditionAccessPaths },
9237
9865
  knownCallSites: 0,
9238
9866
  dynamicCallSites: 0
9239
9867
  });
9240
9868
  }
9241
9869
  const conditionalHookModes = buildUniqueFeatureAliases(records, conditionalHookModesByModule);
9870
+ const reactHookModules = buildUniqueFeatureAliases(records, findReactHookModules(records));
9242
9871
  collectConditionalHookModeCallSites(records, conditionalHookModes);
9243
9872
  let changed = true;
9244
9873
  while (changed) {
@@ -9262,6 +9891,7 @@ function buildProjectModel(root, candidates, options = {}) {
9262
9891
  bindingCompatibleComponentProps: buildUniqueFeatureAliases(records, bindingCompatibleComponentPropsByModule),
9263
9892
  staticIterationTables: buildUniqueFeatureAliases(records, staticIterationTablesByModule),
9264
9893
  conditionalHookModes,
9894
+ reactHookModules,
9265
9895
  sourceEffects: new Map
9266
9896
  };
9267
9897
  }
@@ -13635,5 +14265,5 @@ ${renderUpdateNotice(update.current, update.latest, colorized)}
13635
14265
  }
13636
14266
  main();
13637
14267
 
13638
- //# debugId=569DB065E837196F64756E2164756E21
14268
+ //# debugId=C71A438CE8D868EF64756E2164756E21
13639
14269
  //# sourceMappingURL=cli.js.map