@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/CHANGELOG.md +13 -0
- package/dist/cli.js +881 -251
- package/dist/cli.js.map +13 -13
- package/dist/scan-worker.js +761 -242
- package/dist/scan-worker.js.map +11 -11
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
|
1425
|
-
return
|
|
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
|
|
1432
|
-
const
|
|
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(
|
|
1519
|
-
return
|
|
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
|
|
1532
|
-
if (
|
|
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
|
|
1545
|
-
return STABLE_ENGINE_FACTORY_MEMBERS.has(finalCallMember(
|
|
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
|
|
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
|
|
1664
|
-
if (
|
|
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
|
|
1780
|
+
const path6 = resolvePath(rawPath, aliases, reactNamespaces, reactRobloxNamespaces);
|
|
1700
1781
|
const args = call.childForFieldName("arguments")?.namedChildren ?? [];
|
|
1701
|
-
if (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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
|
|
1773
|
-
if (
|
|
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 (
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
2437
|
-
return
|
|
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(
|
|
2506
|
-
const normalized =
|
|
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(
|
|
2656
|
-
const normalized =
|
|
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
|
|
2677
|
-
const reason = knownYieldReason(
|
|
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(
|
|
2684
|
-
const normalized =
|
|
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(
|
|
2711
|
-
const final =
|
|
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(
|
|
2788
|
-
return
|
|
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
|
|
3044
|
-
if (ALWAYS_MANAGED_METHOD.test(
|
|
3226
|
+
const path7 = context.getCallPath(current) ?? "";
|
|
3227
|
+
if (ALWAYS_MANAGED_METHOD.test(path7))
|
|
3045
3228
|
return true;
|
|
3046
|
-
if (GENERIC_MANAGER_METHOD.test(
|
|
3047
|
-
const receiver =
|
|
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(
|
|
3153
|
-
if (
|
|
3335
|
+
function taskSeverity(path7, call, taskCallback, yieldPoint, context) {
|
|
3336
|
+
if (path7 === "task.defer" && !yieldPoint)
|
|
3154
3337
|
return "suggestion";
|
|
3155
|
-
if (
|
|
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
|
|
3206
|
-
const taskCall =
|
|
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)` :
|
|
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
|
|
3444
|
+
const path7 = context.resolveCallPath(rawPath);
|
|
3262
3445
|
const variable = assignedVariableName(call, callback);
|
|
3263
|
-
const finalMember =
|
|
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 (
|
|
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 (
|
|
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 =
|
|
3528
|
+
const outlivesEffect = path7 !== "task.spawn" || Boolean(yieldPoint);
|
|
3346
3529
|
if (!outlivesEffect)
|
|
3347
3530
|
return null;
|
|
3348
3531
|
return {
|
|
3349
3532
|
call,
|
|
3350
|
-
label:
|
|
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(
|
|
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
|
|
3380
|
-
if (
|
|
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
|
|
3630
|
+
const path7 = context.resolveCallPath(context.getCallPath(call) ?? "");
|
|
3436
3631
|
const args = context.callArguments(call);
|
|
3437
|
-
if (
|
|
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
|
|
3468
|
-
if (/:Connect$/.test(
|
|
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
|
|
3525
|
-
if (!isEffectPath(
|
|
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
|
|
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
|
|
3568
|
-
if (!isEffectPath(
|
|
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 (
|
|
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
|
-
|
|
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}()
|
|
3584
|
-
help: "
|
|
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
|
|
3601
|
-
if (!HOOKS_WITH_DEPS.has(
|
|
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: `${
|
|
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
|
|
3631
|
-
if (!HOOKS_WITH_DEPS.has(
|
|
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
|
-
|
|
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(
|
|
3817
|
-
const normalized = normalizeExpressionText(
|
|
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
|
|
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(
|
|
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
|
|
3917
|
-
if (!PURE_DERIVED_CALL.test(
|
|
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
|
|
3997
|
-
if (
|
|
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
|
|
4023
|
-
if (
|
|
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
|
|
4083
|
-
const final =
|
|
4084
|
-
return !
|
|
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
|
|
4095
|
-
if (
|
|
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
|
|
4126
|
-
const final =
|
|
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
|
|
4137
|
-
return !/^(?:Color3|Vector2|Vector3|UDim|UDim2|CFrame|TweenInfo|NumberSequence|ColorSequence)\./.test(
|
|
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
|
|
4143
|
-
const final =
|
|
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
|
|
4178
|
-
const shape = HOOK_ARGUMENTS.get(
|
|
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
|
-
|
|
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: `${
|
|
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: `${
|
|
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: `${
|
|
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: `${
|
|
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
|
|
4280
|
-
if (
|
|
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
|
|
4337
|
-
if (
|
|
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
|
|
4374
|
-
if (
|
|
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}(
|
|
7267
|
-
note: "A custom hook may choose different internal hooks only when
|
|
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
|
|
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
|
|
7944
|
+
function reachableReturnsBefore(call, fn, context, cache) {
|
|
7455
7945
|
if (!fn.body)
|
|
7456
|
-
return
|
|
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.
|
|
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
|
|
7574
|
-
|
|
7575
|
-
const
|
|
7576
|
-
const
|
|
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
|
|
7579
|
-
const
|
|
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:
|
|
7583
|
-
message: `Hook ${rawPath} receives
|
|
7584
|
-
help: `
|
|
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 &&
|
|
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
|
-
|
|
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
|
|
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
|
|
8455
|
-
new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*not\\s
|
|
8456
|
-
new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s
|
|
8457
|
-
new RegExp(`^\\s*local\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*(?:true|false|nil)\\s*(?:==|~=)\\s
|
|
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
|
-
|
|
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
|
|
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=
|
|
14268
|
+
//# debugId=C71A438CE8D868EF64756E2164756E21
|
|
13639
14269
|
//# sourceMappingURL=cli.js.map
|