@colrealpro/react-luau-doctor 0.17.5 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1146 -246
- package/dist/cli.js.map +18 -17
- package/dist/scan-worker.js +1129 -246
- package/dist/scan-worker.js.map +15 -14
- package/package.json +2 -3
- package/docs/analysis.md +0 -37
- package/docs/ci.md +0 -71
- package/docs/cli.md +0 -115
- package/docs/development.md +0 -63
- package/docs/rules.md +0 -56
package/dist/scan-worker.js
CHANGED
|
@@ -1290,16 +1290,175 @@ var INSTANCE_FACTORY_MEMBERS2 = new Set([
|
|
|
1290
1290
|
function finalCallMember2(path3) {
|
|
1291
1291
|
return path3.split(/[.:]/).at(-1) ?? path3;
|
|
1292
1292
|
}
|
|
1293
|
-
function
|
|
1293
|
+
function declarationInitializer(node, name) {
|
|
1294
|
+
if (node.type !== "variable_declaration")
|
|
1295
|
+
return;
|
|
1296
|
+
const assignment = node.namedChildren.find((child) => child.type === "assignment_statement");
|
|
1297
|
+
const variables = assignment?.namedChildren.find((child) => child.type === "variable_list") ?? node.namedChildren.find((child) => child.type === "variable_list");
|
|
1298
|
+
const expressions = assignment?.namedChildren.find((child) => child.type === "expression_list");
|
|
1299
|
+
const names = variables?.namedChildren.filter((child) => child.type === "identifier") ?? [];
|
|
1300
|
+
const index = names.findIndex((candidate) => candidate.text === name);
|
|
1301
|
+
if (index < 0)
|
|
1302
|
+
return;
|
|
1303
|
+
return expressions?.namedChildren[index] ?? expressions?.namedChildren[0] ?? null;
|
|
1304
|
+
}
|
|
1305
|
+
function functionBindsName(node, name) {
|
|
1306
|
+
if (node.type !== "function_definition" && node.type !== "function_declaration")
|
|
1307
|
+
return false;
|
|
1308
|
+
const parameters = node.childForFieldName("parameters");
|
|
1309
|
+
if (!parameters)
|
|
1310
|
+
return false;
|
|
1311
|
+
return parameters.namedChildren.some((parameter) => parameter.namedChildren.some((child) => child.type === "identifier" && child.text === name) || parameter.type === "identifier" && parameter.text === name);
|
|
1312
|
+
}
|
|
1313
|
+
function loopBindsName(node, name) {
|
|
1314
|
+
if (node.type !== "for_statement")
|
|
1315
|
+
return false;
|
|
1316
|
+
const header = node.text.split(/\bdo\b/s, 1)[0] ?? "";
|
|
1317
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1318
|
+
return new RegExp(`^\\s*for\\s+(?:${escaped}\\s*=|[^\\n]*\\b${escaped}\\b[^\\n]*\\bin\\b)`, "s").test(header);
|
|
1319
|
+
}
|
|
1320
|
+
function visibleInitializer(name, from, context) {
|
|
1321
|
+
let current = from.parent;
|
|
1322
|
+
while (current) {
|
|
1323
|
+
if (current.type === "block" || current.id === context.root.id) {
|
|
1324
|
+
const children = current.namedChildren;
|
|
1325
|
+
for (let index = children.length - 1;index >= 0; index -= 1) {
|
|
1326
|
+
const child = children[index];
|
|
1327
|
+
if (child.startIndex >= from.startIndex)
|
|
1328
|
+
continue;
|
|
1329
|
+
const initializer = declarationInitializer(child, name);
|
|
1330
|
+
if (initializer !== undefined)
|
|
1331
|
+
return initializer;
|
|
1332
|
+
if (child.type === "function_declaration") {
|
|
1333
|
+
const declared = child.childForFieldName("name")?.text.replace(/\s+/g, "") ?? "";
|
|
1334
|
+
if (declared === name)
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (functionBindsName(current, name) || loopBindsName(current, name))
|
|
1340
|
+
return null;
|
|
1341
|
+
current = current.parent;
|
|
1342
|
+
}
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
function expressionHasRobloxOrigin(expression, from, context, seen) {
|
|
1346
|
+
if (!expression)
|
|
1347
|
+
return false;
|
|
1348
|
+
const normalized = expression.text.replace(/\s+/g, "");
|
|
1349
|
+
if (/^(?:game|workspace|script)(?:[.:]|$)/.test(normalized) || /^Instance\.new\(/.test(normalized))
|
|
1350
|
+
return true;
|
|
1351
|
+
const root = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*)/)?.[1];
|
|
1352
|
+
if (!root || root === "require" || seen.has(root))
|
|
1353
|
+
return false;
|
|
1354
|
+
if (expression.type !== "identifier" && expression.type !== "function_call")
|
|
1355
|
+
return false;
|
|
1356
|
+
seen.add(root);
|
|
1357
|
+
return expressionHasRobloxOrigin(visibleInitializer(root, from, context), from, context, seen);
|
|
1358
|
+
}
|
|
1359
|
+
function hasRobloxReceiver(path3, call, context) {
|
|
1360
|
+
const normalized = path3.replace(/\s+/g, "");
|
|
1361
|
+
if (/^(?:game|workspace|script)(?:[.:]|$)/.test(normalized))
|
|
1362
|
+
return true;
|
|
1363
|
+
const root = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*)/)?.[1];
|
|
1364
|
+
if (!root)
|
|
1365
|
+
return false;
|
|
1366
|
+
return expressionHasRobloxOrigin(visibleInitializer(root, call, context), call, context, new Set([root]));
|
|
1367
|
+
}
|
|
1368
|
+
var runServiceCallCache = new WeakMap;
|
|
1369
|
+
var runServiceExpressionCache = new WeakMap;
|
|
1370
|
+
function cachedNodeBoolean(cache, context, node, compute) {
|
|
1371
|
+
let entries = cache.get(context);
|
|
1372
|
+
if (!entries) {
|
|
1373
|
+
entries = new Map;
|
|
1374
|
+
cache.set(context, entries);
|
|
1375
|
+
}
|
|
1376
|
+
const cached = entries.get(node.id);
|
|
1377
|
+
if (cached !== undefined)
|
|
1378
|
+
return cached;
|
|
1379
|
+
const value = compute();
|
|
1380
|
+
entries.set(node.id, value);
|
|
1381
|
+
return value;
|
|
1382
|
+
}
|
|
1383
|
+
var RUN_SERVICE_EVENTS = new Set([
|
|
1384
|
+
"RenderStepped",
|
|
1385
|
+
"Heartbeat",
|
|
1386
|
+
"Stepped",
|
|
1387
|
+
"PreRender",
|
|
1388
|
+
"PreSimulation",
|
|
1389
|
+
"PostSimulation"
|
|
1390
|
+
]);
|
|
1391
|
+
function expressionIsRunService(expression, from, context, seen) {
|
|
1392
|
+
if (!expression)
|
|
1393
|
+
return false;
|
|
1394
|
+
const normalized = expression.text.replace(/\s+/g, "");
|
|
1395
|
+
if (/^game:GetService\(["']RunService["']\)$/.test(normalized))
|
|
1396
|
+
return true;
|
|
1397
|
+
if (expression.type !== "identifier")
|
|
1398
|
+
return false;
|
|
1399
|
+
const name = expression.text;
|
|
1400
|
+
if (seen.has(name))
|
|
1401
|
+
return false;
|
|
1402
|
+
seen.add(name);
|
|
1403
|
+
return expressionIsRunService(visibleInitializer(name, from, context), from, context, seen);
|
|
1404
|
+
}
|
|
1405
|
+
function rootIsRunService(root, from, context) {
|
|
1406
|
+
if (root === "game")
|
|
1407
|
+
return false;
|
|
1408
|
+
return expressionIsRunService(visibleInitializer(root, from, context), from, context, new Set([root]));
|
|
1409
|
+
}
|
|
1410
|
+
function isHighFrequencyRunServiceExpression(node, context) {
|
|
1411
|
+
return cachedNodeBoolean(runServiceExpressionCache, context, node, () => {
|
|
1412
|
+
const normalized = node.text.replace(/\s+/g, "");
|
|
1413
|
+
if (/^game:GetService\(["']RunService["']\)\.(?:RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation)$/.test(normalized)) {
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
const match = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*)\.(RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation)$/);
|
|
1417
|
+
return Boolean(match && RUN_SERVICE_EVENTS.has(match[2]) && rootIsRunService(match[1], node, context));
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
function isHighFrequencyRunServiceCall(call, context) {
|
|
1421
|
+
return cachedNodeBoolean(runServiceCallCache, context, call, () => {
|
|
1422
|
+
const normalized = (context.getCallPath(call) ?? "").replace(/\s+/g, "");
|
|
1423
|
+
const direct = call.text.replace(/\s+/g, "");
|
|
1424
|
+
if (/^game:GetService\(["']RunService["']\)(?:\.(?:RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation):Connect|:BindToRenderStep|:BindToSimulation)\(/.test(direct)) {
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
const event = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*)\.(RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation):Connect$/);
|
|
1428
|
+
if (event)
|
|
1429
|
+
return rootIsRunService(event[1], call, context);
|
|
1430
|
+
const bind = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*):(?:BindToRenderStep|BindToSimulation)$/);
|
|
1431
|
+
return Boolean(bind && rootIsRunService(bind[1], call, context));
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
function sourceHasHighFrequencyRunService(source) {
|
|
1435
|
+
if (/game\s*:\s*GetService\s*\(\s*["']RunService["']\s*\)\s*(?:\.\s*(?:RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation)\s*:\s*Connect|:\s*(?:BindToRenderStep|BindToSimulation))/.test(source)) {
|
|
1436
|
+
return true;
|
|
1437
|
+
}
|
|
1438
|
+
const aliases = new Set;
|
|
1439
|
+
for (const match of source.matchAll(/\blocal\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*game\s*:\s*GetService\s*\(\s*["']RunService["']\s*\)/g)) {
|
|
1440
|
+
aliases.add(match[1]);
|
|
1441
|
+
}
|
|
1442
|
+
for (const alias of aliases) {
|
|
1443
|
+
const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1444
|
+
const pattern = new RegExp(`\\b${escaped}\\s*(?:\\.\\s*(?:RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation)\\s*:\\s*Connect|:\\s*(?:BindToRenderStep|BindToSimulation))`);
|
|
1445
|
+
if (pattern.test(source))
|
|
1446
|
+
return true;
|
|
1447
|
+
}
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
function knownYieldReason(path3, call, context) {
|
|
1294
1451
|
const normalized = path3.replace(/\s+/g, "");
|
|
1295
1452
|
if (normalized === "task.wait")
|
|
1296
1453
|
return "task.wait";
|
|
1297
1454
|
if (normalized === "coroutine.yield")
|
|
1298
1455
|
return "coroutine.yield";
|
|
1299
1456
|
const member = finalCallMember2(normalized);
|
|
1300
|
-
if (YIELDING_ENGINE_MEMBERS.has(member))
|
|
1301
|
-
return
|
|
1302
|
-
|
|
1457
|
+
if (!YIELDING_ENGINE_MEMBERS.has(member))
|
|
1458
|
+
return null;
|
|
1459
|
+
if (!call || !context)
|
|
1460
|
+
return null;
|
|
1461
|
+
return hasRobloxReceiver(normalized, call, context) ? member : null;
|
|
1303
1462
|
}
|
|
1304
1463
|
function functionYieldPoint(node, context) {
|
|
1305
1464
|
for (const candidate of context.walk(node)) {
|
|
@@ -1310,7 +1469,7 @@ function functionYieldPoint(node, context) {
|
|
|
1310
1469
|
if (owner && nearest !== owner)
|
|
1311
1470
|
continue;
|
|
1312
1471
|
const path3 = context.resolveCallPath(context.getCallPath(candidate) ?? "");
|
|
1313
|
-
const reason = knownYieldReason(path3);
|
|
1472
|
+
const reason = knownYieldReason(path3, candidate, context);
|
|
1314
1473
|
if (reason)
|
|
1315
1474
|
return { call: candidate, reason };
|
|
1316
1475
|
}
|
|
@@ -1511,6 +1670,38 @@ function isNameShadowedBetween(node, boundary, name) {
|
|
|
1511
1670
|
}
|
|
1512
1671
|
return false;
|
|
1513
1672
|
}
|
|
1673
|
+
var topLevelBindingsCache = new WeakMap;
|
|
1674
|
+
function topLevelBindings(boundary) {
|
|
1675
|
+
const cached = topLevelBindingsCache.get(boundary);
|
|
1676
|
+
if (cached)
|
|
1677
|
+
return cached;
|
|
1678
|
+
const result = new Map;
|
|
1679
|
+
const add = (name, node) => {
|
|
1680
|
+
const existing = result.get(name) ?? [];
|
|
1681
|
+
existing.push(node);
|
|
1682
|
+
result.set(name, existing);
|
|
1683
|
+
};
|
|
1684
|
+
for (const child of boundary.body?.namedChildren ?? []) {
|
|
1685
|
+
if (child.type === "variable_declaration") {
|
|
1686
|
+
for (const name of declarationNames2(child))
|
|
1687
|
+
add(name, child);
|
|
1688
|
+
} else if (child.type === "function_declaration") {
|
|
1689
|
+
const declared = child.childForFieldName("name")?.text.replace(/\s+/g, "") ?? "";
|
|
1690
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(declared))
|
|
1691
|
+
add(declared, child);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
topLevelBindingsCache.set(boundary, result);
|
|
1695
|
+
return result;
|
|
1696
|
+
}
|
|
1697
|
+
function isBindingShadowedBetween(node, boundary, name, declaration = null) {
|
|
1698
|
+
if (isNameShadowedBetween(node, boundary, name))
|
|
1699
|
+
return true;
|
|
1700
|
+
if (!boundary.body)
|
|
1701
|
+
return false;
|
|
1702
|
+
const afterIndex = declaration?.endIndex ?? boundary.body.startIndex - 1;
|
|
1703
|
+
return (topLevelBindings(boundary).get(name) ?? []).some((binding) => binding.startIndex > afterIndex && binding.startIndex < node.startIndex);
|
|
1704
|
+
}
|
|
1514
1705
|
function containsUnshadowedIdentifier(node, name, context, boundary) {
|
|
1515
1706
|
if (!node)
|
|
1516
1707
|
return false;
|
|
@@ -3019,6 +3210,343 @@ var noNestedComponentDefinition = {
|
|
|
3019
3210
|
}
|
|
3020
3211
|
};
|
|
3021
3212
|
|
|
3213
|
+
// src/module-resolution.ts
|
|
3214
|
+
import path3 from "path";
|
|
3215
|
+
function normalizeRelative(value) {
|
|
3216
|
+
return value.split(path3.sep).join("/");
|
|
3217
|
+
}
|
|
3218
|
+
function moduleKeys(relativePath) {
|
|
3219
|
+
let normalized = normalizeRelative(relativePath).replace(/\.(?:lua|luau)$/i, "");
|
|
3220
|
+
if (normalized.endsWith("/init"))
|
|
3221
|
+
normalized = normalized.slice(0, -"/init".length);
|
|
3222
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
3223
|
+
if (segments[0]?.toLowerCase() === "src")
|
|
3224
|
+
segments.shift();
|
|
3225
|
+
const keys = new Set;
|
|
3226
|
+
for (let index = 0;index < segments.length; index += 1) {
|
|
3227
|
+
const suffix = segments.slice(index).join(".").toLowerCase();
|
|
3228
|
+
if (suffix)
|
|
3229
|
+
keys.add(suffix);
|
|
3230
|
+
}
|
|
3231
|
+
return [...keys];
|
|
3232
|
+
}
|
|
3233
|
+
function normalizeRequireTarget(text) {
|
|
3234
|
+
return (text.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? []).join(".").toLowerCase();
|
|
3235
|
+
}
|
|
3236
|
+
function buildUniqueFeatureAliases(modules, featureByModuleId) {
|
|
3237
|
+
const owners = new Map;
|
|
3238
|
+
for (const module of modules) {
|
|
3239
|
+
for (const key of module.keys) {
|
|
3240
|
+
const ids = owners.get(key) ?? new Set;
|
|
3241
|
+
ids.add(module.id);
|
|
3242
|
+
owners.set(key, ids);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
const aliases = new Map;
|
|
3246
|
+
for (const module of modules) {
|
|
3247
|
+
const value = featureByModuleId.get(module.id);
|
|
3248
|
+
if (value === undefined)
|
|
3249
|
+
continue;
|
|
3250
|
+
for (const key of module.keys) {
|
|
3251
|
+
const ids = owners.get(key);
|
|
3252
|
+
if (ids?.size === 1 && ids.has(module.id))
|
|
3253
|
+
aliases.set(key, value);
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
return aliases;
|
|
3257
|
+
}
|
|
3258
|
+
function resolveModuleReference(target, aliases) {
|
|
3259
|
+
let candidate = target;
|
|
3260
|
+
while (true) {
|
|
3261
|
+
if (aliases.has(candidate))
|
|
3262
|
+
return aliases.get(candidate);
|
|
3263
|
+
const separator = candidate.indexOf(".");
|
|
3264
|
+
if (separator < 0)
|
|
3265
|
+
return null;
|
|
3266
|
+
candidate = candidate.slice(separator + 1);
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
// src/rules/parameter-mutations.ts
|
|
3271
|
+
var analysisCache = new WeakMap;
|
|
3272
|
+
var EMPTY_INDEXES = new Set;
|
|
3273
|
+
function declarationParts(node) {
|
|
3274
|
+
if (node.type !== "variable_declaration")
|
|
3275
|
+
return { names: [], expressions: [] };
|
|
3276
|
+
const assignment = node.namedChildren.find((child) => child.type === "assignment_statement");
|
|
3277
|
+
const variables = assignment?.namedChildren.find((child) => child.type === "variable_list") ?? node.namedChildren.find((child) => child.type === "variable_list");
|
|
3278
|
+
const expressions = assignment?.namedChildren.find((child) => child.type === "expression_list");
|
|
3279
|
+
return {
|
|
3280
|
+
names: variables?.namedChildren.filter((child) => child.type === "identifier").map((child) => child.text) ?? [],
|
|
3281
|
+
expressions: expressions?.namedChildren ?? []
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
function assignmentParts(node) {
|
|
3285
|
+
if (node.type !== "assignment_statement" || node.parent?.type === "variable_declaration") {
|
|
3286
|
+
return { names: [], expressions: [] };
|
|
3287
|
+
}
|
|
3288
|
+
const variables = node.namedChildren.find((child) => child.type === "variable_list");
|
|
3289
|
+
const expressions = node.namedChildren.find((child) => child.type === "expression_list");
|
|
3290
|
+
return {
|
|
3291
|
+
names: variables?.namedChildren.filter((child) => child.type === "identifier").map((child) => child.text) ?? [],
|
|
3292
|
+
expressions: expressions?.namedChildren ?? []
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3295
|
+
function expressionForName(node, name) {
|
|
3296
|
+
const { names, expressions } = node.type === "variable_declaration" ? declarationParts(node) : assignmentParts(node);
|
|
3297
|
+
const index = names.indexOf(name);
|
|
3298
|
+
if (index < 0)
|
|
3299
|
+
return;
|
|
3300
|
+
return expressions[index] ?? expressions[0] ?? null;
|
|
3301
|
+
}
|
|
3302
|
+
function currentModuleSummary(context) {
|
|
3303
|
+
for (const key of moduleKeys(context.relativePath)) {
|
|
3304
|
+
const summary = context.project.sourceEffects.get(key);
|
|
3305
|
+
if (summary)
|
|
3306
|
+
return summary;
|
|
3307
|
+
}
|
|
3308
|
+
return null;
|
|
3309
|
+
}
|
|
3310
|
+
function topLevelImports(context) {
|
|
3311
|
+
const result = new Map;
|
|
3312
|
+
for (const node of context.root.namedChildren) {
|
|
3313
|
+
if (node.type !== "variable_declaration")
|
|
3314
|
+
continue;
|
|
3315
|
+
const { names, expressions } = declarationParts(node);
|
|
3316
|
+
for (let index = 0;index < names.length; index += 1) {
|
|
3317
|
+
const expression = expressions[index] ?? expressions[0];
|
|
3318
|
+
if (expression?.type !== "function_call")
|
|
3319
|
+
continue;
|
|
3320
|
+
const match = expression.text.match(/^\s*require\s*\((.*?)\)\s*$/s);
|
|
3321
|
+
if (!match)
|
|
3322
|
+
continue;
|
|
3323
|
+
const summary = resolveModuleReference(normalizeRequireTarget(match[1]), context.project.sourceEffects);
|
|
3324
|
+
if (summary)
|
|
3325
|
+
result.set(names[index], { declaration: node, summary });
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
return result;
|
|
3329
|
+
}
|
|
3330
|
+
function analysisFor(context) {
|
|
3331
|
+
const cached = analysisCache.get(context);
|
|
3332
|
+
if (cached)
|
|
3333
|
+
return cached;
|
|
3334
|
+
const imports = topLevelImports(context);
|
|
3335
|
+
const mutatingFactoryMembers = new Set;
|
|
3336
|
+
for (const { summary } of imports.values()) {
|
|
3337
|
+
if (summary.instanceFactories.size === 0)
|
|
3338
|
+
continue;
|
|
3339
|
+
for (const member of summary.mutatingMemberParameters.keys())
|
|
3340
|
+
mutatingFactoryMembers.add(member);
|
|
3341
|
+
}
|
|
3342
|
+
const result = {
|
|
3343
|
+
currentSummary: currentModuleSummary(context),
|
|
3344
|
+
imports,
|
|
3345
|
+
mutatingFactoryMembers,
|
|
3346
|
+
callIndexes: new Map,
|
|
3347
|
+
originCache: new Map
|
|
3348
|
+
};
|
|
3349
|
+
analysisCache.set(context, result);
|
|
3350
|
+
return result;
|
|
3351
|
+
}
|
|
3352
|
+
function directChildContaining(block, node) {
|
|
3353
|
+
for (const child of block.namedChildren) {
|
|
3354
|
+
if (child.startIndex <= node.startIndex && child.endIndex >= node.endIndex)
|
|
3355
|
+
return child;
|
|
3356
|
+
}
|
|
3357
|
+
return null;
|
|
3358
|
+
}
|
|
3359
|
+
function visibleBinding(name, node, owner) {
|
|
3360
|
+
let current = node;
|
|
3361
|
+
while (current && !sameNode(current, owner.node)) {
|
|
3362
|
+
const parent = current.parent;
|
|
3363
|
+
if (parent?.type === "block") {
|
|
3364
|
+
const containing = directChildContaining(parent, node);
|
|
3365
|
+
const beforeIndex = containing?.startIndex ?? node.startIndex;
|
|
3366
|
+
const children = parent.namedChildren;
|
|
3367
|
+
for (let index = children.length - 1;index >= 0; index -= 1) {
|
|
3368
|
+
const child = children[index];
|
|
3369
|
+
if (child.startIndex >= beforeIndex)
|
|
3370
|
+
continue;
|
|
3371
|
+
if (child.type !== "variable_declaration" && child.type !== "assignment_statement")
|
|
3372
|
+
continue;
|
|
3373
|
+
const expression = expressionForName(child, name);
|
|
3374
|
+
if (expression !== undefined)
|
|
3375
|
+
return { node: child, expression };
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
current = parent;
|
|
3379
|
+
}
|
|
3380
|
+
return null;
|
|
3381
|
+
}
|
|
3382
|
+
function isTopLevelFunctionVisible(context, name, owner) {
|
|
3383
|
+
for (const node of context.root.namedChildren) {
|
|
3384
|
+
if (node.startIndex >= owner.node.startIndex)
|
|
3385
|
+
break;
|
|
3386
|
+
if (node.type === "function_declaration") {
|
|
3387
|
+
const declared = node.childForFieldName("name")?.text.replace(/\s+/g, "") ?? "";
|
|
3388
|
+
if (declared === name)
|
|
3389
|
+
return true;
|
|
3390
|
+
continue;
|
|
3391
|
+
}
|
|
3392
|
+
if (node.type !== "variable_declaration")
|
|
3393
|
+
continue;
|
|
3394
|
+
const { names, expressions } = declarationParts(node);
|
|
3395
|
+
const index = names.indexOf(name);
|
|
3396
|
+
if (index < 0)
|
|
3397
|
+
continue;
|
|
3398
|
+
if ((expressions[index] ?? expressions[0])?.type === "function_definition")
|
|
3399
|
+
return true;
|
|
3400
|
+
}
|
|
3401
|
+
return false;
|
|
3402
|
+
}
|
|
3403
|
+
function moduleBindingVisible(binding, name, call, owner) {
|
|
3404
|
+
if (binding.declaration.startIndex >= owner.node.startIndex)
|
|
3405
|
+
return false;
|
|
3406
|
+
if (owner.parameters.includes(name))
|
|
3407
|
+
return false;
|
|
3408
|
+
return visibleBinding(name, call, owner) === null;
|
|
3409
|
+
}
|
|
3410
|
+
function builtinMutatedParameterIndexes(path4, argumentCount) {
|
|
3411
|
+
switch (path4) {
|
|
3412
|
+
case "rawset":
|
|
3413
|
+
case "setmetatable":
|
|
3414
|
+
case "table.clear":
|
|
3415
|
+
case "table.freeze":
|
|
3416
|
+
case "table.insert":
|
|
3417
|
+
case "table.remove":
|
|
3418
|
+
case "table.sort":
|
|
3419
|
+
return argumentCount > 0 ? new Set([0]) : EMPTY_INDEXES;
|
|
3420
|
+
case "table.move":
|
|
3421
|
+
return argumentCount >= 5 ? new Set([4]) : argumentCount > 0 ? new Set([0]) : EMPTY_INDEXES;
|
|
3422
|
+
default:
|
|
3423
|
+
return EMPTY_INDEXES;
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
function importedFactorySummary(context, receiver, call, owner, analysis) {
|
|
3427
|
+
const binding = visibleBinding(receiver, call, owner);
|
|
3428
|
+
if (!binding?.expression)
|
|
3429
|
+
return null;
|
|
3430
|
+
const expression = binding.expression;
|
|
3431
|
+
if (expression.type === "identifier") {
|
|
3432
|
+
return importedFactorySummary(context, expression.text, expression, owner, analysis);
|
|
3433
|
+
}
|
|
3434
|
+
if (expression.type !== "function_call")
|
|
3435
|
+
return null;
|
|
3436
|
+
const path4 = normalizeExpressionText(context.getCallPath(expression) ?? "");
|
|
3437
|
+
const match = path4.match(/^([A-Za-z_][A-Za-z0-9_]*)[.:]([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
3438
|
+
if (!match)
|
|
3439
|
+
return null;
|
|
3440
|
+
const imported = analysis.imports.get(match[1]);
|
|
3441
|
+
if (!imported || !moduleBindingVisible(imported, match[1], expression, owner))
|
|
3442
|
+
return null;
|
|
3443
|
+
return imported.summary.instanceFactories.has(match[2]) ? imported.summary : null;
|
|
3444
|
+
}
|
|
3445
|
+
function mightMutateParameters(context, call) {
|
|
3446
|
+
const path4 = normalizeExpressionText(context.getCallPath(call) ?? "");
|
|
3447
|
+
if (builtinMutatedParameterIndexes(path4, context.callArguments(call).length).size > 0)
|
|
3448
|
+
return true;
|
|
3449
|
+
const analysis = analysisFor(context);
|
|
3450
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(path4)) {
|
|
3451
|
+
if ((analysis.currentSummary?.localMutatingParameters.get(path4)?.size ?? 0) > 0)
|
|
3452
|
+
return true;
|
|
3453
|
+
return (analysis.imports.get(path4)?.summary.mutatingExportParameters.size ?? 0) > 0;
|
|
3454
|
+
}
|
|
3455
|
+
const member = path4.match(/^([A-Za-z_][A-Za-z0-9_]*)[.:]([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
3456
|
+
if (!member)
|
|
3457
|
+
return false;
|
|
3458
|
+
if ((analysis.imports.get(member[1])?.summary.mutatingMemberParameters.get(member[2])?.size ?? 0) > 0)
|
|
3459
|
+
return true;
|
|
3460
|
+
return analysis.mutatingFactoryMembers.has(member[2]);
|
|
3461
|
+
}
|
|
3462
|
+
function mutatedParameterIndexesForCall(context, call, owner) {
|
|
3463
|
+
const analysis = analysisFor(context);
|
|
3464
|
+
const cached = analysis.callIndexes.get(call.id);
|
|
3465
|
+
if (cached)
|
|
3466
|
+
return cached;
|
|
3467
|
+
const path4 = normalizeExpressionText(context.getCallPath(call) ?? "");
|
|
3468
|
+
const builtin = builtinMutatedParameterIndexes(path4, context.callArguments(call).length);
|
|
3469
|
+
if (builtin.size > 0) {
|
|
3470
|
+
analysis.callIndexes.set(call.id, builtin);
|
|
3471
|
+
return builtin;
|
|
3472
|
+
}
|
|
3473
|
+
let result = EMPTY_INDEXES;
|
|
3474
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(path4)) {
|
|
3475
|
+
const importBinding = analysis.imports.get(path4);
|
|
3476
|
+
if (importBinding && moduleBindingVisible(importBinding, path4, call, owner)) {
|
|
3477
|
+
result = importBinding.summary.mutatingExportParameters;
|
|
3478
|
+
} else if (!owner.parameters.includes(path4) && visibleBinding(path4, call, owner) === null && isTopLevelFunctionVisible(context, path4, owner)) {
|
|
3479
|
+
result = analysis.currentSummary?.localMutatingParameters.get(path4) ?? EMPTY_INDEXES;
|
|
3480
|
+
}
|
|
3481
|
+
} else {
|
|
3482
|
+
const member = path4.match(/^([A-Za-z_][A-Za-z0-9_]*)([.:])([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
3483
|
+
if (member) {
|
|
3484
|
+
const imported = analysis.imports.get(member[1]);
|
|
3485
|
+
if (imported && moduleBindingVisible(imported, member[1], call, owner)) {
|
|
3486
|
+
result = imported.summary.mutatingMemberParameters.get(member[3]) ?? EMPTY_INDEXES;
|
|
3487
|
+
} else {
|
|
3488
|
+
const factory = importedFactorySummary(context, member[1], call, owner, analysis);
|
|
3489
|
+
result = factory?.mutatingMemberParameters.get(member[3]) ?? EMPTY_INDEXES;
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
analysis.callIndexes.set(call.id, result);
|
|
3494
|
+
return result;
|
|
3495
|
+
}
|
|
3496
|
+
function freshExpression(context, expression) {
|
|
3497
|
+
if (expression.type === "table_constructor" || expression.type === "function_definition")
|
|
3498
|
+
return true;
|
|
3499
|
+
if (expression.type !== "function_call")
|
|
3500
|
+
return false;
|
|
3501
|
+
const path4 = normalizeExpressionText(context.getCallPath(expression) ?? "");
|
|
3502
|
+
if (path4 === "table.clone" || path4 === "table.create" || path4 === "table.pack")
|
|
3503
|
+
return true;
|
|
3504
|
+
if (path4 === "setmetatable") {
|
|
3505
|
+
const first = context.callArguments(expression)[0];
|
|
3506
|
+
return Boolean(first && freshExpression(context, first));
|
|
3507
|
+
}
|
|
3508
|
+
return false;
|
|
3509
|
+
}
|
|
3510
|
+
function stateBindingForName(context, owner, name, bindingNode) {
|
|
3511
|
+
return context.model.stateBindings.find((binding) => binding.owner === owner && binding.valueName === name && sameNode(binding.declaration, bindingNode)) ?? null;
|
|
3512
|
+
}
|
|
3513
|
+
function resolveNameOrigin(context, name, atNode, owner, seen) {
|
|
3514
|
+
const key = `${owner.node.id}:${name}:${atNode.startIndex}`;
|
|
3515
|
+
if (seen.has(key))
|
|
3516
|
+
return { kind: "unknown" };
|
|
3517
|
+
seen.add(key);
|
|
3518
|
+
const binding = visibleBinding(name, atNode, owner);
|
|
3519
|
+
if (binding) {
|
|
3520
|
+
const state = stateBindingForName(context, owner, name, binding.node);
|
|
3521
|
+
if (state)
|
|
3522
|
+
return { kind: "state", binding: state };
|
|
3523
|
+
if (!binding.expression)
|
|
3524
|
+
return { kind: "unknown" };
|
|
3525
|
+
return resolveExpressionOriginInternal(context, binding.expression, owner, seen);
|
|
3526
|
+
}
|
|
3527
|
+
if (owner.isComponent && owner.parameters[0] === name)
|
|
3528
|
+
return { kind: "props", name };
|
|
3529
|
+
return { kind: "unknown" };
|
|
3530
|
+
}
|
|
3531
|
+
function resolveExpressionOriginInternal(context, expression, owner, seen) {
|
|
3532
|
+
if (freshExpression(context, expression))
|
|
3533
|
+
return { kind: "fresh" };
|
|
3534
|
+
const root = rootIdentifier(expression.text);
|
|
3535
|
+
if (!root)
|
|
3536
|
+
return { kind: "unknown" };
|
|
3537
|
+
return resolveNameOrigin(context, root, expression, owner, seen);
|
|
3538
|
+
}
|
|
3539
|
+
function mutationOriginForExpression(context, expression, owner) {
|
|
3540
|
+
const analysis = analysisFor(context);
|
|
3541
|
+
const key = `${owner.node.id}:${expression.id}`;
|
|
3542
|
+
const cached = analysis.originCache.get(key);
|
|
3543
|
+
if (cached)
|
|
3544
|
+
return cached;
|
|
3545
|
+
const result = resolveExpressionOriginInternal(context, expression, owner, new Set);
|
|
3546
|
+
analysis.originCache.set(key, result);
|
|
3547
|
+
return result;
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3022
3550
|
// src/rules/no-prop-mutation.ts
|
|
3023
3551
|
var noPropMutation = {
|
|
3024
3552
|
id: "react-luau/no-prop-mutation",
|
|
@@ -3028,7 +3556,27 @@ var noPropMutation = {
|
|
|
3028
3556
|
run(context) {
|
|
3029
3557
|
const diagnostics = [];
|
|
3030
3558
|
for (const node of context.walk()) {
|
|
3031
|
-
if (node.type
|
|
3559
|
+
if (node.type === "assignment_statement" || node.type === "update_statement") {
|
|
3560
|
+
const component2 = context.containingComponent(node);
|
|
3561
|
+
if (!component2 || !context.isDirectlyExecutedInFunction(node, component2))
|
|
3562
|
+
continue;
|
|
3563
|
+
const propsName2 = component2.parameters[0];
|
|
3564
|
+
if (!propsName2)
|
|
3565
|
+
continue;
|
|
3566
|
+
if (isBindingShadowedBetween(node, component2, propsName2))
|
|
3567
|
+
continue;
|
|
3568
|
+
const left = assignmentLeft(node.text);
|
|
3569
|
+
const escaped = propsName2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3570
|
+
if (!new RegExp(`^${escaped}\\s*(?:\\.|\\[)`).test(left))
|
|
3571
|
+
continue;
|
|
3572
|
+
diagnostics.push({
|
|
3573
|
+
node: assignmentTargetNode(node),
|
|
3574
|
+
message: `Component mutates ${propsName2} directly.`,
|
|
3575
|
+
help: "Treat props as immutable. Derive a local value, clone a table you own, or update state in the owner instead."
|
|
3576
|
+
});
|
|
3577
|
+
continue;
|
|
3578
|
+
}
|
|
3579
|
+
if (node.type !== "function_call" || !mightMutateParameters(context, node))
|
|
3032
3580
|
continue;
|
|
3033
3581
|
const component = context.containingComponent(node);
|
|
3034
3582
|
if (!component || !context.isDirectlyExecutedInFunction(node, component))
|
|
@@ -3036,16 +3584,27 @@ var noPropMutation = {
|
|
|
3036
3584
|
const propsName = component.parameters[0];
|
|
3037
3585
|
if (!propsName)
|
|
3038
3586
|
continue;
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
const
|
|
3043
|
-
|
|
3587
|
+
const arguments_ = context.callArguments(node);
|
|
3588
|
+
const mutatedIndexes = mutatedParameterIndexesForCall(context, node, component);
|
|
3589
|
+
let mutatedArgument = null;
|
|
3590
|
+
for (const index of mutatedIndexes) {
|
|
3591
|
+
const argument = arguments_[index];
|
|
3592
|
+
if (!argument)
|
|
3593
|
+
continue;
|
|
3594
|
+
const origin = mutationOriginForExpression(context, argument, component);
|
|
3595
|
+
if (origin.kind !== "props" || origin.name !== propsName)
|
|
3596
|
+
continue;
|
|
3597
|
+
mutatedArgument = argument;
|
|
3598
|
+
break;
|
|
3599
|
+
}
|
|
3600
|
+
if (!mutatedArgument)
|
|
3044
3601
|
continue;
|
|
3602
|
+
const path4 = context.resolveCallPath(context.getCallPath(node) ?? "") || "This call";
|
|
3045
3603
|
diagnostics.push({
|
|
3046
|
-
node:
|
|
3047
|
-
|
|
3048
|
-
|
|
3604
|
+
node: callNameNode(node),
|
|
3605
|
+
highlights: [mutatedArgument],
|
|
3606
|
+
message: `${path4} mutates an argument derived from ${propsName}.`,
|
|
3607
|
+
help: "Treat props as immutable. Clone the table before passing it to a mutating helper, or move the mutation into the owner that controls the value."
|
|
3049
3608
|
});
|
|
3050
3609
|
}
|
|
3051
3610
|
return diagnostics;
|
|
@@ -3069,9 +3628,9 @@ function freshDescription(node, context) {
|
|
|
3069
3628
|
for (const child of context.walk(node)) {
|
|
3070
3629
|
if (child.type !== "function_call")
|
|
3071
3630
|
continue;
|
|
3072
|
-
const
|
|
3073
|
-
if (FRESH_CALLS.some((pattern) => pattern.test(
|
|
3074
|
-
return `${
|
|
3631
|
+
const path4 = context.getCallPath(child) ?? "";
|
|
3632
|
+
if (FRESH_CALLS.some((pattern) => pattern.test(path4)) || /Random\.new\s*\([^)]*\)\s*:\s*Next(?:Integer|Number)/s.test(child.text)) {
|
|
3633
|
+
return `${path4 || "fresh-value call"}()`;
|
|
3075
3634
|
}
|
|
3076
3635
|
}
|
|
3077
3636
|
return null;
|
|
@@ -3126,18 +3685,20 @@ var noSetStateInRender = {
|
|
|
3126
3685
|
for (const component of context.model.functions) {
|
|
3127
3686
|
if (!component.isComponent || !component.body)
|
|
3128
3687
|
continue;
|
|
3129
|
-
const
|
|
3688
|
+
const bindings = stateBindingsFor(context, component);
|
|
3689
|
+
const setters = new Map(bindings.map((binding) => [binding.setterName, binding]));
|
|
3130
3690
|
if (setters.size === 0)
|
|
3131
3691
|
continue;
|
|
3132
3692
|
for (const statement of component.body.namedChildren) {
|
|
3133
3693
|
if (statement.type !== "function_call")
|
|
3134
3694
|
continue;
|
|
3135
|
-
const
|
|
3136
|
-
|
|
3695
|
+
const path4 = context.getCallPath(statement);
|
|
3696
|
+
const binding = path4 ? setters.get(path4) : undefined;
|
|
3697
|
+
if (!path4 || !binding || isBindingShadowedBetween(statement, component, path4, binding.declaration))
|
|
3137
3698
|
continue;
|
|
3138
3699
|
diagnostics.push({
|
|
3139
3700
|
node: callNameNode(statement),
|
|
3140
|
-
message: `${
|
|
3701
|
+
message: `${path4}() is called unconditionally during component render.`,
|
|
3141
3702
|
help: "Move the update to the event or effect that owns it, or derive the value during render. An unconditional render-phase state update can continuously trigger new renders."
|
|
3142
3703
|
});
|
|
3143
3704
|
}
|
|
@@ -3168,115 +3729,57 @@ var TYPE_SYNTAX_ANCESTORS = new Set([
|
|
|
3168
3729
|
"variadic_type_pack"
|
|
3169
3730
|
]);
|
|
3170
3731
|
function incompleteDelimitedNode(node) {
|
|
3171
|
-
const closer = REQUIRED_CLOSERS.get(node.type);
|
|
3172
|
-
if (!closer)
|
|
3173
|
-
return null;
|
|
3174
|
-
return node.text.trimEnd().endsWith(closer) ? null : closer;
|
|
3175
|
-
}
|
|
3176
|
-
function isBundledGrammarTypeGap(node) {
|
|
3177
|
-
let current = node;
|
|
3178
|
-
while (current) {
|
|
3179
|
-
if (TYPE_SYNTAX_ANCESTORS.has(current.type)) {
|
|
3180
|
-
if (current.type !== "type_definition")
|
|
3181
|
-
return true;
|
|
3182
|
-
if (/^\s*(?:export\s+)?type\s+[A-Za-z_][A-Za-z0-9_]*/s.test(current.text))
|
|
3183
|
-
return true;
|
|
3184
|
-
}
|
|
3185
|
-
current = current.parent;
|
|
3186
|
-
}
|
|
3187
|
-
return false;
|
|
3188
|
-
}
|
|
3189
|
-
var parseErrors = {
|
|
3190
|
-
id: "react-luau/parse-error",
|
|
3191
|
-
category: "Correctness",
|
|
3192
|
-
severity: "error",
|
|
3193
|
-
description: "Report executable Luau syntax that the bundled parser cannot form into a complete syntax tree.",
|
|
3194
|
-
run(context) {
|
|
3195
|
-
const diagnostics = [];
|
|
3196
|
-
const covered = [];
|
|
3197
|
-
for (const node of context.walk()) {
|
|
3198
|
-
const missingCloser = incompleteDelimitedNode(node);
|
|
3199
|
-
if (!node.isError && !node.isMissing && !missingCloser)
|
|
3200
|
-
continue;
|
|
3201
|
-
if (isBundledGrammarTypeGap(node))
|
|
3202
|
-
continue;
|
|
3203
|
-
if (covered.some((range) => node.startIndex >= range.start && node.endIndex <= range.end))
|
|
3204
|
-
continue;
|
|
3205
|
-
covered.push({ start: node.startIndex, end: node.endIndex });
|
|
3206
|
-
let message = "Luau syntax could not be parsed cleanly at this location.";
|
|
3207
|
-
if (node.isMissing)
|
|
3208
|
-
message = `Luau syntax is missing ${node.type}.`;
|
|
3209
|
-
else if (missingCloser)
|
|
3210
|
-
message = `Luau syntax is missing closing ${missingCloser}.`;
|
|
3211
|
-
diagnostics.push({
|
|
3212
|
-
node,
|
|
3213
|
-
message,
|
|
3214
|
-
help: "Fix the syntax error before relying on downstream React-Luau diagnostics in this file."
|
|
3215
|
-
});
|
|
3216
|
-
}
|
|
3217
|
-
return diagnostics;
|
|
3218
|
-
}
|
|
3219
|
-
};
|
|
3220
|
-
|
|
3221
|
-
// src/module-resolution.ts
|
|
3222
|
-
import path3 from "path";
|
|
3223
|
-
function normalizeRelative(value) {
|
|
3224
|
-
return value.split(path3.sep).join("/");
|
|
3225
|
-
}
|
|
3226
|
-
function moduleKeys(relativePath) {
|
|
3227
|
-
let normalized = normalizeRelative(relativePath).replace(/\.(?:lua|luau)$/i, "");
|
|
3228
|
-
if (normalized.endsWith("/init"))
|
|
3229
|
-
normalized = normalized.slice(0, -"/init".length);
|
|
3230
|
-
const segments = normalized.split("/").filter(Boolean);
|
|
3231
|
-
if (segments[0]?.toLowerCase() === "src")
|
|
3232
|
-
segments.shift();
|
|
3233
|
-
const keys = new Set;
|
|
3234
|
-
for (let index = 0;index < segments.length; index += 1) {
|
|
3235
|
-
const suffix = segments.slice(index).join(".").toLowerCase();
|
|
3236
|
-
if (suffix)
|
|
3237
|
-
keys.add(suffix);
|
|
3238
|
-
}
|
|
3239
|
-
return [...keys];
|
|
3240
|
-
}
|
|
3241
|
-
function normalizeRequireTarget(text) {
|
|
3242
|
-
return (text.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? []).join(".").toLowerCase();
|
|
3243
|
-
}
|
|
3244
|
-
function buildUniqueFeatureAliases(modules, featureByModuleId) {
|
|
3245
|
-
const owners = new Map;
|
|
3246
|
-
for (const module of modules) {
|
|
3247
|
-
for (const key of module.keys) {
|
|
3248
|
-
const ids = owners.get(key) ?? new Set;
|
|
3249
|
-
ids.add(module.id);
|
|
3250
|
-
owners.set(key, ids);
|
|
3251
|
-
}
|
|
3252
|
-
}
|
|
3253
|
-
const aliases = new Map;
|
|
3254
|
-
for (const module of modules) {
|
|
3255
|
-
const value = featureByModuleId.get(module.id);
|
|
3256
|
-
if (value === undefined)
|
|
3257
|
-
continue;
|
|
3258
|
-
for (const key of module.keys) {
|
|
3259
|
-
const ids = owners.get(key);
|
|
3260
|
-
if (ids?.size === 1 && ids.has(module.id))
|
|
3261
|
-
aliases.set(key, value);
|
|
3732
|
+
const closer = REQUIRED_CLOSERS.get(node.type);
|
|
3733
|
+
if (!closer)
|
|
3734
|
+
return null;
|
|
3735
|
+
return node.text.trimEnd().endsWith(closer) ? null : closer;
|
|
3736
|
+
}
|
|
3737
|
+
function isBundledGrammarTypeGap(node) {
|
|
3738
|
+
let current = node;
|
|
3739
|
+
while (current) {
|
|
3740
|
+
if (TYPE_SYNTAX_ANCESTORS.has(current.type)) {
|
|
3741
|
+
if (current.type !== "type_definition")
|
|
3742
|
+
return true;
|
|
3743
|
+
if (/^\s*(?:export\s+)?type\s+[A-Za-z_][A-Za-z0-9_]*/s.test(current.text))
|
|
3744
|
+
return true;
|
|
3262
3745
|
}
|
|
3746
|
+
current = current.parent;
|
|
3263
3747
|
}
|
|
3264
|
-
return
|
|
3748
|
+
return false;
|
|
3265
3749
|
}
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3750
|
+
var parseErrors = {
|
|
3751
|
+
id: "react-luau/parse-error",
|
|
3752
|
+
category: "Correctness",
|
|
3753
|
+
severity: "error",
|
|
3754
|
+
description: "Report executable Luau syntax that the bundled parser cannot form into a complete syntax tree.",
|
|
3755
|
+
run(context) {
|
|
3756
|
+
const diagnostics = [];
|
|
3757
|
+
const covered = [];
|
|
3758
|
+
for (const node of context.walk()) {
|
|
3759
|
+
const missingCloser = incompleteDelimitedNode(node);
|
|
3760
|
+
if (!node.isError && !node.isMissing && !missingCloser)
|
|
3761
|
+
continue;
|
|
3762
|
+
if (isBundledGrammarTypeGap(node))
|
|
3763
|
+
continue;
|
|
3764
|
+
if (covered.some((range) => node.startIndex >= range.start && node.endIndex <= range.end))
|
|
3765
|
+
continue;
|
|
3766
|
+
covered.push({ start: node.startIndex, end: node.endIndex });
|
|
3767
|
+
let message = "Luau syntax could not be parsed cleanly at this location.";
|
|
3768
|
+
if (node.isMissing)
|
|
3769
|
+
message = `Luau syntax is missing ${node.type}.`;
|
|
3770
|
+
else if (missingCloser)
|
|
3771
|
+
message = `Luau syntax is missing closing ${missingCloser}.`;
|
|
3772
|
+
diagnostics.push({
|
|
3773
|
+
node,
|
|
3774
|
+
message,
|
|
3775
|
+
help: "Fix the syntax error before relying on downstream React-Luau diagnostics in this file."
|
|
3776
|
+
});
|
|
3777
|
+
}
|
|
3778
|
+
return diagnostics;
|
|
3275
3779
|
}
|
|
3276
|
-
}
|
|
3780
|
+
};
|
|
3277
3781
|
|
|
3278
3782
|
// src/rules/performance-rules.ts
|
|
3279
|
-
var HIGH_FREQUENCY = /(?:RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation)\s*:\s*Connect|BindToRenderStep|BindToSimulation/;
|
|
3280
3783
|
var STATIC_DISCOVERY = /(?::|\.)(GetChildren|GetDescendants)$/;
|
|
3281
3784
|
var PURE_TRIVIAL_CALLS = /^(?:tostring|tonumber|type|typeof|math\.[A-Za-z_][A-Za-z0-9_]*|string\.(?:lower|upper|format|len)|Color3\.(?:new|fromRGB|fromHSV)|Vector[23]\.new|UDim2?\.(?:new|fromOffset|fromScale)|CFrame\.new)$/;
|
|
3282
3785
|
function declarationForCall(call) {
|
|
@@ -3314,7 +3817,7 @@ function identifierReferences(context, owner, name, declaration) {
|
|
|
3314
3817
|
continue;
|
|
3315
3818
|
if (isIdentifierPropertyName(node))
|
|
3316
3819
|
continue;
|
|
3317
|
-
if (
|
|
3820
|
+
if (isBindingShadowedBetween(node, owner, name, declaration))
|
|
3318
3821
|
continue;
|
|
3319
3822
|
result.push(node);
|
|
3320
3823
|
}
|
|
@@ -3329,17 +3832,14 @@ function nearestAncestor(node, stop, type) {
|
|
|
3329
3832
|
}
|
|
3330
3833
|
return null;
|
|
3331
3834
|
}
|
|
3332
|
-
function highFrequencyCallback(node) {
|
|
3835
|
+
function highFrequencyCallback(node, context) {
|
|
3333
3836
|
let current = node.parent;
|
|
3334
3837
|
while (current) {
|
|
3335
3838
|
if (current.type === "function_definition") {
|
|
3336
3839
|
const argumentsNode = current.parent;
|
|
3337
3840
|
const call = argumentsNode?.type === "arguments" ? argumentsNode.parent : null;
|
|
3338
|
-
if (call?.type === "function_call")
|
|
3339
|
-
|
|
3340
|
-
if (HIGH_FREQUENCY.test(name))
|
|
3341
|
-
return current;
|
|
3342
|
-
}
|
|
3841
|
+
if (call?.type === "function_call" && isHighFrequencyRunServiceCall(call, context))
|
|
3842
|
+
return current;
|
|
3343
3843
|
}
|
|
3344
3844
|
current = current.parent;
|
|
3345
3845
|
}
|
|
@@ -3477,7 +3977,7 @@ var rerenderHighFrequencyState = {
|
|
|
3477
3977
|
const diagnostics = [];
|
|
3478
3978
|
const seen = new Set;
|
|
3479
3979
|
for (const call of context.findCalls()) {
|
|
3480
|
-
const callback = highFrequencyCallback(call);
|
|
3980
|
+
const callback = highFrequencyCallback(call, context);
|
|
3481
3981
|
if (!callback)
|
|
3482
3982
|
continue;
|
|
3483
3983
|
const component = context.containingComponent(call);
|
|
@@ -3770,7 +4270,6 @@ var preferUseRefForMutableCell = {
|
|
|
3770
4270
|
};
|
|
3771
4271
|
|
|
3772
4272
|
// src/rules/prefer-binding-over-state.ts
|
|
3773
|
-
var HIGH_FREQUENCY2 = /(?:RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation)\s*:\s*Connect|BindToRenderStep|BindToSimulation/;
|
|
3774
4273
|
var EXTERNAL_CALLBACK = /(?:^|[.:])(?:Connect|Once|Subscribe|Observe|Listen|Watch)$/i;
|
|
3775
4274
|
var NON_BINDABLE_HOST_FIELDS = new Set(["ref", "key", "children"]);
|
|
3776
4275
|
function replaceWithinNode(container, target, replacement) {
|
|
@@ -3925,7 +4424,7 @@ function isReadNode(node, valueName, owner, declaration) {
|
|
|
3925
4424
|
return false;
|
|
3926
4425
|
if (isIdentifierPropertyName2(node))
|
|
3927
4426
|
return false;
|
|
3928
|
-
if (
|
|
4427
|
+
if (isBindingShadowedBetween(node, owner, valueName, declaration))
|
|
3929
4428
|
return false;
|
|
3930
4429
|
const parent = node.parent;
|
|
3931
4430
|
if (parent?.type === "variable_list" || parent?.type === "typed_identifier")
|
|
@@ -4030,9 +4529,9 @@ function stateReadsAreBindingCompatible(valueName, owner, declaration, context,
|
|
|
4030
4529
|
}
|
|
4031
4530
|
return reads > 0;
|
|
4032
4531
|
}
|
|
4033
|
-
function callbackSource(
|
|
4034
|
-
const normalized =
|
|
4035
|
-
const highFrequency =
|
|
4532
|
+
function callbackSource(call, context) {
|
|
4533
|
+
const normalized = (context.getCallPath(call) ?? "").replace(/\s+/g, "");
|
|
4534
|
+
const highFrequency = isHighFrequencyRunServiceCall(call, context);
|
|
4036
4535
|
const final = normalized.split(/[.:]/).at(-1) ?? normalized;
|
|
4037
4536
|
const external = highFrequency || EXTERNAL_CALLBACK.test(normalized) || EXTERNAL_CALLBACK.test(final);
|
|
4038
4537
|
return { highFrequency, external };
|
|
@@ -4047,7 +4546,7 @@ function importedCallbackSource(call, callbackArgument, importedCallbacks, conte
|
|
|
4047
4546
|
if (callbackIndex < 0 || !summary.callbackParameterIndexes.includes(callbackIndex))
|
|
4048
4547
|
return null;
|
|
4049
4548
|
return {
|
|
4050
|
-
highFrequency: summary.highFrequency || args.some((arg) =>
|
|
4549
|
+
highFrequency: summary.highFrequency || args.some((arg) => isHighFrequencyRunServiceExpression(arg, context)),
|
|
4051
4550
|
external: true
|
|
4052
4551
|
};
|
|
4053
4552
|
}
|
|
@@ -4059,8 +4558,7 @@ function directCallbackSource(node, importedCallbacks, context) {
|
|
|
4059
4558
|
const imported = importedCallbackSource(call, node, importedCallbacks, context);
|
|
4060
4559
|
if (imported)
|
|
4061
4560
|
return imported;
|
|
4062
|
-
const
|
|
4063
|
-
const source = callbackSource(raw);
|
|
4561
|
+
const source = callbackSource(call, context);
|
|
4064
4562
|
return source.external ? source : null;
|
|
4065
4563
|
}
|
|
4066
4564
|
function namedFunctionCallbackSource(fn, owner, importedCallbacks, context) {
|
|
@@ -4076,7 +4574,7 @@ function namedFunctionCallbackSource(fn, owner, importedCallbacks, context) {
|
|
|
4076
4574
|
if (!callbackArg)
|
|
4077
4575
|
continue;
|
|
4078
4576
|
const imported = importedCallbackSource(call, callbackArg, importedCallbacks, context);
|
|
4079
|
-
const source = imported ?? callbackSource(
|
|
4577
|
+
const source = imported ?? callbackSource(call, context);
|
|
4080
4578
|
if (!source.external)
|
|
4081
4579
|
continue;
|
|
4082
4580
|
foundExternal = true;
|
|
@@ -4559,6 +5057,26 @@ function assignmentRight(text) {
|
|
|
4559
5057
|
const match = text.match(/^(?:.*?)(?:\+=|-=|\*=|\/=|%=|\^=|\.\.=|=)\s*(.+)$/s);
|
|
4560
5058
|
return match?.[1]?.trim() ?? null;
|
|
4561
5059
|
}
|
|
5060
|
+
function refDeclaration(owner, refName, context) {
|
|
5061
|
+
if (!owner.body)
|
|
5062
|
+
return null;
|
|
5063
|
+
for (const statement of owner.body.namedChildren) {
|
|
5064
|
+
if (statement.type !== "variable_declaration")
|
|
5065
|
+
continue;
|
|
5066
|
+
const names = declarationNames2(statement);
|
|
5067
|
+
const index = names.indexOf(refName);
|
|
5068
|
+
if (index === -1)
|
|
5069
|
+
continue;
|
|
5070
|
+
const assignment = statement.namedChildren.find((child) => child.type === "assignment_statement");
|
|
5071
|
+
const expressions = assignment?.namedChildren.find((child) => child.type === "expression_list")?.namedChildren ?? [];
|
|
5072
|
+
const expression = expressions[index] ?? expressions[0];
|
|
5073
|
+
if (!expression || expression.type !== "function_call")
|
|
5074
|
+
continue;
|
|
5075
|
+
if (context.resolveCallPath(context.getCallPath(expression) ?? "") === "React.useRef")
|
|
5076
|
+
return statement;
|
|
5077
|
+
}
|
|
5078
|
+
return null;
|
|
5079
|
+
}
|
|
4562
5080
|
function refInitializer(owner, refName, context) {
|
|
4563
5081
|
if (!owner.body)
|
|
4564
5082
|
return null;
|
|
@@ -4655,6 +5173,9 @@ var noRefCurrentInRender = {
|
|
|
4655
5173
|
const refName = refRootFromTarget(target, refs);
|
|
4656
5174
|
if (!refName || isNilGuardedLazyInit(node, refName))
|
|
4657
5175
|
continue;
|
|
5176
|
+
const declaration = refDeclaration(owner, refName, context);
|
|
5177
|
+
if (isBindingShadowedBetween(node, owner, refName, declaration))
|
|
5178
|
+
continue;
|
|
4658
5179
|
const latestValueMirror = isLatestValueMirror(node, owner, refName, context);
|
|
4659
5180
|
diagnostics.push({
|
|
4660
5181
|
node: assignmentTargetNode(node),
|
|
@@ -4668,7 +5189,7 @@ var noRefCurrentInRender = {
|
|
|
4668
5189
|
};
|
|
4669
5190
|
|
|
4670
5191
|
// src/rules/render-side-effects.ts
|
|
4671
|
-
function
|
|
5192
|
+
function declarationParts2(node) {
|
|
4672
5193
|
if (node.type !== "variable_declaration")
|
|
4673
5194
|
return { names: [], expressions: [] };
|
|
4674
5195
|
const assignment = node.namedChildren.find((child) => child.type === "assignment_statement");
|
|
@@ -4679,17 +5200,103 @@ function declarationParts(node) {
|
|
|
4679
5200
|
expressions: expressions?.namedChildren ?? []
|
|
4680
5201
|
};
|
|
4681
5202
|
}
|
|
5203
|
+
function assignmentParts2(node) {
|
|
5204
|
+
if (node.type !== "assignment_statement" || node.parent?.type === "variable_declaration")
|
|
5205
|
+
return { names: [], expressions: [] };
|
|
5206
|
+
const variables = node.namedChildren.find((child) => child.type === "variable_list");
|
|
5207
|
+
const expressions = node.namedChildren.find((child) => child.type === "expression_list");
|
|
5208
|
+
return {
|
|
5209
|
+
names: variables?.namedChildren.filter((child) => child.type === "identifier").map((child) => child.text) ?? [],
|
|
5210
|
+
expressions: expressions?.namedChildren ?? []
|
|
5211
|
+
};
|
|
5212
|
+
}
|
|
5213
|
+
function bindingParts(node) {
|
|
5214
|
+
return node.type === "variable_declaration" ? declarationParts2(node) : assignmentParts2(node);
|
|
5215
|
+
}
|
|
5216
|
+
function addSourceBinding(bindings, name, declaration, value) {
|
|
5217
|
+
const existing = bindings.get(name) ?? [];
|
|
5218
|
+
existing.push({ declaration, value });
|
|
5219
|
+
bindings.set(name, existing);
|
|
5220
|
+
}
|
|
5221
|
+
function nearestScopeContainer(node, context) {
|
|
5222
|
+
const owner = context.nearestFunction(node);
|
|
5223
|
+
let current = node.parent;
|
|
5224
|
+
while (current) {
|
|
5225
|
+
if (current.type === "block")
|
|
5226
|
+
return current;
|
|
5227
|
+
if (owner && current.id === owner.node.id)
|
|
5228
|
+
return owner.body ?? owner.node;
|
|
5229
|
+
current = current.parent;
|
|
5230
|
+
}
|
|
5231
|
+
return context.root;
|
|
5232
|
+
}
|
|
5233
|
+
function declarationVisibleAt(declaration, node, context) {
|
|
5234
|
+
if (declaration.startIndex >= node.startIndex)
|
|
5235
|
+
return false;
|
|
5236
|
+
const container = nearestScopeContainer(declaration, context);
|
|
5237
|
+
return node.startIndex >= container.startIndex && node.endIndex <= container.endIndex;
|
|
5238
|
+
}
|
|
5239
|
+
function functionParameterNames(node) {
|
|
5240
|
+
const result = new Set;
|
|
5241
|
+
const parameters = node.childForFieldName("parameters") ?? node.namedChildren.find((child) => child.type === "parameters");
|
|
5242
|
+
for (const parameter of parameters?.namedChildren ?? []) {
|
|
5243
|
+
if (parameter.type === "identifier")
|
|
5244
|
+
result.add(parameter.text);
|
|
5245
|
+
for (const child of parameter.namedChildren) {
|
|
5246
|
+
if (child.type === "identifier")
|
|
5247
|
+
result.add(child.text);
|
|
5248
|
+
}
|
|
5249
|
+
}
|
|
5250
|
+
return result;
|
|
5251
|
+
}
|
|
5252
|
+
function loopBindsName2(node, name) {
|
|
5253
|
+
if (node.type !== "for_statement")
|
|
5254
|
+
return false;
|
|
5255
|
+
const header = node.text.split(/\bdo\b/s, 1)[0] ?? "";
|
|
5256
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5257
|
+
return new RegExp(`^\\s*for\\s+(?:${escaped}\\s*=|[^\\n]*\\b${escaped}\\b[^\\n]*\\bin\\b)`, "s").test(header);
|
|
5258
|
+
}
|
|
5259
|
+
function bindingShadowedAfterDeclaration(declaration, node, name) {
|
|
5260
|
+
let current = node.parent;
|
|
5261
|
+
while (current) {
|
|
5262
|
+
if (declaration.startIndex >= current.startIndex && declaration.endIndex <= current.endIndex)
|
|
5263
|
+
return false;
|
|
5264
|
+
if ((current.type === "function_definition" || current.type === "function_declaration") && functionParameterNames(current).has(name))
|
|
5265
|
+
return true;
|
|
5266
|
+
if (loopBindsName2(current, name))
|
|
5267
|
+
return true;
|
|
5268
|
+
current = current.parent;
|
|
5269
|
+
}
|
|
5270
|
+
return false;
|
|
5271
|
+
}
|
|
5272
|
+
function resolveSourceBinding(bindings, name, node, context) {
|
|
5273
|
+
const candidates = bindings.get(name);
|
|
5274
|
+
if (!candidates)
|
|
5275
|
+
return null;
|
|
5276
|
+
for (let index = candidates.length - 1;index >= 0; index -= 1) {
|
|
5277
|
+
const candidate = candidates[index];
|
|
5278
|
+
if (!declarationVisibleAt(candidate.declaration, node, context))
|
|
5279
|
+
continue;
|
|
5280
|
+
if (bindingShadowedAfterDeclaration(candidate.declaration, node, name))
|
|
5281
|
+
return null;
|
|
5282
|
+
return candidate.value;
|
|
5283
|
+
}
|
|
5284
|
+
return null;
|
|
5285
|
+
}
|
|
4682
5286
|
function sourceEffectImports(context) {
|
|
4683
5287
|
const result = new Map;
|
|
4684
5288
|
for (const node of context.walk(context.root)) {
|
|
4685
|
-
if (node.type !== "variable_declaration")
|
|
5289
|
+
if (node.type !== "variable_declaration" && node.type !== "assignment_statement")
|
|
4686
5290
|
continue;
|
|
4687
|
-
|
|
4688
|
-
if (!match)
|
|
5291
|
+
if (node.type === "assignment_statement" && node.parent?.type === "variable_declaration")
|
|
4689
5292
|
continue;
|
|
4690
|
-
const
|
|
4691
|
-
|
|
4692
|
-
|
|
5293
|
+
const { names, expressions } = bindingParts(node);
|
|
5294
|
+
for (let index = 0;index < names.length; index += 1) {
|
|
5295
|
+
const expression = expressions[index] ?? expressions[0];
|
|
5296
|
+
const requireMatch = expression?.type === "function_call" ? expression.text.match(/^\s*require\s*\((.*?)\)\s*$/s) : null;
|
|
5297
|
+
const summary = requireMatch ? resolveModuleReference(normalizeRequireTarget(requireMatch[1]), context.project.sourceEffects) : null;
|
|
5298
|
+
addSourceBinding(result, names[index], node, summary);
|
|
5299
|
+
}
|
|
4693
5300
|
}
|
|
4694
5301
|
return result;
|
|
4695
5302
|
}
|
|
@@ -4698,7 +5305,7 @@ function factorySummaryFromCall(call, context, imports) {
|
|
|
4698
5305
|
const match = path4.match(/^([A-Za-z_][A-Za-z0-9_]*)[.:]([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
4699
5306
|
if (!match)
|
|
4700
5307
|
return null;
|
|
4701
|
-
const summary = imports
|
|
5308
|
+
const summary = resolveSourceBinding(imports, match[1], call, context);
|
|
4702
5309
|
if (!summary?.instanceFactories.has(match[2]))
|
|
4703
5310
|
return null;
|
|
4704
5311
|
return summary;
|
|
@@ -4719,61 +5326,57 @@ function returnedFactorySummary(callback, context, imports) {
|
|
|
4719
5326
|
function sourceEffectInstances(context, imports) {
|
|
4720
5327
|
const result = new Map;
|
|
4721
5328
|
for (const node of context.walk(context.root)) {
|
|
4722
|
-
if (node.type !== "variable_declaration")
|
|
5329
|
+
if (node.type !== "variable_declaration" && node.type !== "assignment_statement")
|
|
4723
5330
|
continue;
|
|
4724
|
-
|
|
5331
|
+
if (node.type === "assignment_statement" && node.parent?.type === "variable_declaration")
|
|
5332
|
+
continue;
|
|
5333
|
+
const { names, expressions } = bindingParts(node);
|
|
4725
5334
|
for (let index = 0;index < names.length; index += 1) {
|
|
4726
5335
|
const expression = expressions[index] ?? expressions[0];
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
5336
|
+
let instance = null;
|
|
5337
|
+
if (expression?.type === "function_call") {
|
|
5338
|
+
const directFactory = factorySummaryFromCall(expression, context, imports);
|
|
5339
|
+
if (directFactory) {
|
|
5340
|
+
instance = {
|
|
5341
|
+
summary: directFactory,
|
|
5342
|
+
persistent: context.nearestFunction(node) === null
|
|
5343
|
+
};
|
|
5344
|
+
} else if (context.resolveCallPath(context.getCallPath(expression) ?? "") === "React.useMemo") {
|
|
4733
5345
|
const callback = context.callArguments(expression)[0];
|
|
4734
|
-
if (callback?.type === "function_definition")
|
|
4735
|
-
|
|
5346
|
+
if (callback?.type === "function_definition") {
|
|
5347
|
+
const memoizedFactory = returnedFactorySummary(callback, context, imports);
|
|
5348
|
+
if (memoizedFactory)
|
|
5349
|
+
instance = { summary: memoizedFactory, persistent: true };
|
|
5350
|
+
}
|
|
4736
5351
|
}
|
|
4737
|
-
} else if (expression
|
|
4738
|
-
|
|
4739
|
-
}
|
|
4740
|
-
if (summary)
|
|
4741
|
-
result.set(names[index], summary);
|
|
4742
|
-
}
|
|
4743
|
-
}
|
|
4744
|
-
let changed = true;
|
|
4745
|
-
while (changed) {
|
|
4746
|
-
changed = false;
|
|
4747
|
-
for (const node of context.walk(context.root)) {
|
|
4748
|
-
if (node.type !== "variable_declaration")
|
|
4749
|
-
continue;
|
|
4750
|
-
const { names, expressions } = declarationParts(node);
|
|
4751
|
-
for (let index = 0;index < names.length; index += 1) {
|
|
4752
|
-
if (result.has(names[index]))
|
|
4753
|
-
continue;
|
|
4754
|
-
const expression = expressions[index] ?? expressions[0];
|
|
4755
|
-
if (expression?.type !== "identifier")
|
|
4756
|
-
continue;
|
|
4757
|
-
const summary = result.get(expression.text);
|
|
4758
|
-
if (!summary)
|
|
4759
|
-
continue;
|
|
4760
|
-
result.set(names[index], summary);
|
|
4761
|
-
changed = true;
|
|
5352
|
+
} else if (expression?.type === "identifier") {
|
|
5353
|
+
instance = resolveSourceBinding(result, expression.text, expression, context);
|
|
4762
5354
|
}
|
|
5355
|
+
addSourceBinding(result, names[index], node, instance);
|
|
4763
5356
|
}
|
|
4764
5357
|
}
|
|
4765
5358
|
return result;
|
|
4766
5359
|
}
|
|
4767
5360
|
function sourceInferredEffectCall(call, context, imports, instances) {
|
|
4768
5361
|
const path4 = context.getCallPath(call)?.replace(/\s+/g, "") ?? "";
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
5362
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(path4)) {
|
|
5363
|
+
const direct = resolveSourceBinding(imports, path4, call, context);
|
|
5364
|
+
if (direct?.effectfulExport)
|
|
5365
|
+
return true;
|
|
5366
|
+
}
|
|
4772
5367
|
const member = path4.match(/^([A-Za-z_][A-Za-z0-9_]*)[.:]([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
4773
5368
|
if (!member)
|
|
4774
5369
|
return false;
|
|
4775
|
-
const
|
|
4776
|
-
|
|
5370
|
+
const instance = resolveSourceBinding(instances, member[1], call, context);
|
|
5371
|
+
if (instance) {
|
|
5372
|
+
if (instance.summary.effectfulMembers.has(member[2]))
|
|
5373
|
+
return true;
|
|
5374
|
+
return instance.persistent && instance.summary.mutatingMembers.has(member[2]);
|
|
5375
|
+
}
|
|
5376
|
+
const imported = resolveSourceBinding(imports, member[1], call, context);
|
|
5377
|
+
if (!imported)
|
|
5378
|
+
return false;
|
|
5379
|
+
return imported.effectfulMembers.has(member[2]) || imported.mutatingMembers.has(member[2]);
|
|
4777
5380
|
}
|
|
4778
5381
|
var RENDER_EFFECT_PATTERNS = [
|
|
4779
5382
|
/^Instance\.new$/,
|
|
@@ -4785,7 +5388,6 @@ var RENDER_EFFECT_PATTERNS = [
|
|
|
4785
5388
|
/BindToRenderStep$/,
|
|
4786
5389
|
/BindToSimulation$/,
|
|
4787
5390
|
/:Destroy$/,
|
|
4788
|
-
/:render$/,
|
|
4789
5391
|
/:unmount$/
|
|
4790
5392
|
];
|
|
4791
5393
|
function hasNestedKnownRenderSideEffect(call, context) {
|
|
@@ -4797,7 +5399,7 @@ function hasNestedKnownRenderSideEffect(call, context) {
|
|
|
4797
5399
|
const path4 = context.resolveCallPath(context.getCallPath(node) ?? "");
|
|
4798
5400
|
if (path4 === "task.spawn" || path4 === "task.defer" || path4 === "task.delay")
|
|
4799
5401
|
return true;
|
|
4800
|
-
if (knownYieldReason(path4) || RENDER_EFFECT_PATTERNS.some((pattern) => pattern.test(path4)))
|
|
5402
|
+
if (knownYieldReason(path4, node, context) || RENDER_EFFECT_PATTERNS.some((pattern) => pattern.test(path4)))
|
|
4801
5403
|
return true;
|
|
4802
5404
|
}
|
|
4803
5405
|
return false;
|
|
@@ -4814,7 +5416,7 @@ var noYieldInRender = {
|
|
|
4814
5416
|
if (!component || !context.isDirectlyExecutedInFunction(call, component))
|
|
4815
5417
|
continue;
|
|
4816
5418
|
const path4 = context.resolveCallPath(context.getCallPath(call) ?? "");
|
|
4817
|
-
const reason = knownYieldReason(path4);
|
|
5419
|
+
const reason = knownYieldReason(path4, call, context);
|
|
4818
5420
|
if (!reason)
|
|
4819
5421
|
continue;
|
|
4820
5422
|
diagnostics.push({
|
|
@@ -5447,7 +6049,7 @@ function directlyReadsValue(context, owner, binding) {
|
|
|
5447
6049
|
continue;
|
|
5448
6050
|
if (isNestedFunctionFromOwner(context, node, owner))
|
|
5449
6051
|
continue;
|
|
5450
|
-
if (
|
|
6052
|
+
if (isBindingShadowedBetween(node, owner, binding.valueName, binding.declaration))
|
|
5451
6053
|
continue;
|
|
5452
6054
|
return true;
|
|
5453
6055
|
}
|
|
@@ -5455,7 +6057,7 @@ function directlyReadsValue(context, owner, binding) {
|
|
|
5455
6057
|
if (!fn.body)
|
|
5456
6058
|
continue;
|
|
5457
6059
|
for (const node of context.walk(fn.body)) {
|
|
5458
|
-
if (isIdentifierRead(node, binding.valueName) && !
|
|
6060
|
+
if (isIdentifierRead(node, binding.valueName) && !isBindingShadowedBetween(node, owner, binding.valueName, binding.declaration))
|
|
5459
6061
|
return true;
|
|
5460
6062
|
}
|
|
5461
6063
|
}
|
|
@@ -5635,13 +6237,20 @@ var noDirectStateMutation = {
|
|
|
5635
6237
|
let mutates = false;
|
|
5636
6238
|
if (node.type === "assignment_statement" || node.type === "update_statement") {
|
|
5637
6239
|
mutates = new RegExp(`^\\s*${escaped}\\s*(?:\\.|\\[)`).test(node.text);
|
|
5638
|
-
} else if (node.type === "function_call") {
|
|
5639
|
-
const
|
|
5640
|
-
|
|
5641
|
-
|
|
6240
|
+
} else if (node.type === "function_call" && mightMutateParameters(context, node)) {
|
|
6241
|
+
const arguments_ = context.callArguments(node);
|
|
6242
|
+
for (const index of mutatedParameterIndexesForCall(context, node, owner)) {
|
|
6243
|
+
const argument = arguments_[index];
|
|
6244
|
+
if (!argument)
|
|
6245
|
+
continue;
|
|
6246
|
+
const origin = mutationOriginForExpression(context, argument, owner);
|
|
6247
|
+
if (origin.kind === "state" && origin.binding === binding) {
|
|
6248
|
+
mutates = true;
|
|
6249
|
+
break;
|
|
6250
|
+
}
|
|
5642
6251
|
}
|
|
5643
6252
|
}
|
|
5644
|
-
if (!mutates ||
|
|
6253
|
+
if (!mutates || isBindingShadowedBetween(node, owner, binding.valueName, binding.declaration))
|
|
5645
6254
|
continue;
|
|
5646
6255
|
const key = `${node.startIndex}:${binding.valueName}`;
|
|
5647
6256
|
if (seen.has(key))
|
|
@@ -5834,6 +6443,27 @@ function topLevelReturnName(root) {
|
|
|
5834
6443
|
}
|
|
5835
6444
|
return null;
|
|
5836
6445
|
}
|
|
6446
|
+
function topLevelImports2(root, moduleAliases) {
|
|
6447
|
+
const imports = new Map;
|
|
6448
|
+
for (const node of root.namedChildren) {
|
|
6449
|
+
if (node.type !== "variable_declaration")
|
|
6450
|
+
continue;
|
|
6451
|
+
const { names, expressions } = declarationParts3(node);
|
|
6452
|
+
for (let index = 0;index < names.length; index += 1) {
|
|
6453
|
+
const expression = expressions[index] ?? expressions[0];
|
|
6454
|
+
if (expression?.type !== "function_call")
|
|
6455
|
+
continue;
|
|
6456
|
+
const text = expression.text.trim();
|
|
6457
|
+
const match = text.match(/^require\s*\((.*?)\)\s*$/s);
|
|
6458
|
+
if (!match)
|
|
6459
|
+
continue;
|
|
6460
|
+
const moduleId = resolveModuleReference(normalizeRequireTarget(match[1]), moduleAliases);
|
|
6461
|
+
if (moduleId)
|
|
6462
|
+
imports.set(names[index], moduleId);
|
|
6463
|
+
}
|
|
6464
|
+
}
|
|
6465
|
+
return imports;
|
|
6466
|
+
}
|
|
5837
6467
|
function parameterNames3(node) {
|
|
5838
6468
|
const parameters = child(node, "parameters");
|
|
5839
6469
|
if (!parameters)
|
|
@@ -5848,7 +6478,7 @@ function parameterNames3(node) {
|
|
|
5848
6478
|
}
|
|
5849
6479
|
return result;
|
|
5850
6480
|
}
|
|
5851
|
-
function
|
|
6481
|
+
function declarationParts3(node) {
|
|
5852
6482
|
if (node.type !== "variable_declaration")
|
|
5853
6483
|
return { names: [], expressions: [] };
|
|
5854
6484
|
const assignment = child(node, "assignment_statement");
|
|
@@ -5884,6 +6514,45 @@ function rootIdentifier2(node) {
|
|
|
5884
6514
|
const text = node.text.trim();
|
|
5885
6515
|
return text.match(/^([A-Za-z_][A-Za-z0-9_]*)/)?.[1] ?? null;
|
|
5886
6516
|
}
|
|
6517
|
+
function callArguments(node) {
|
|
6518
|
+
if (node.type !== "function_call")
|
|
6519
|
+
return [];
|
|
6520
|
+
return node.childForFieldName("arguments")?.namedChildren ?? [];
|
|
6521
|
+
}
|
|
6522
|
+
function builtinMutatedArgumentIndexes(path5, argumentCount) {
|
|
6523
|
+
switch (path5.replace(/\s+/g, "")) {
|
|
6524
|
+
case "rawset":
|
|
6525
|
+
case "setmetatable":
|
|
6526
|
+
case "table.clear":
|
|
6527
|
+
case "table.freeze":
|
|
6528
|
+
case "table.insert":
|
|
6529
|
+
case "table.remove":
|
|
6530
|
+
case "table.sort":
|
|
6531
|
+
return argumentCount > 0 ? [0] : [];
|
|
6532
|
+
case "table.move":
|
|
6533
|
+
return argumentCount >= 5 ? [4] : argumentCount > 0 ? [0] : [];
|
|
6534
|
+
default:
|
|
6535
|
+
return [];
|
|
6536
|
+
}
|
|
6537
|
+
}
|
|
6538
|
+
function expressionCreatesOwnedValue(node, owned) {
|
|
6539
|
+
if (node.type === "table_constructor" || node.type === "function_definition")
|
|
6540
|
+
return true;
|
|
6541
|
+
if (node.type !== "function_call")
|
|
6542
|
+
return false;
|
|
6543
|
+
const path5 = callPath(node)?.replace(/\s+/g, "") ?? "";
|
|
6544
|
+
if (path5 === "table.clone" || path5 === "table.create" || path5 === "table.pack")
|
|
6545
|
+
return true;
|
|
6546
|
+
if (path5 !== "setmetatable" && path5 !== "table.freeze")
|
|
6547
|
+
return false;
|
|
6548
|
+
const first = callArguments(node)[0];
|
|
6549
|
+
if (!first)
|
|
6550
|
+
return false;
|
|
6551
|
+
if (first.type === "table_constructor")
|
|
6552
|
+
return true;
|
|
6553
|
+
const root = rootIdentifier2(first);
|
|
6554
|
+
return Boolean(root && owned.has(root));
|
|
6555
|
+
}
|
|
5887
6556
|
function directNodes(body) {
|
|
5888
6557
|
const result = [];
|
|
5889
6558
|
const visit = (node) => {
|
|
@@ -5914,7 +6583,14 @@ function topLevelFunctionRecords(record) {
|
|
|
5914
6583
|
body,
|
|
5915
6584
|
parameters: parameterNames3(node),
|
|
5916
6585
|
directEffect: false,
|
|
5917
|
-
|
|
6586
|
+
mutatesReceiver: false,
|
|
6587
|
+
mutatedParameterIndexes: new Set,
|
|
6588
|
+
dependencies: new Set,
|
|
6589
|
+
mutationCalls: [],
|
|
6590
|
+
parameterOrigins: new Map,
|
|
6591
|
+
externalRoots: new Set,
|
|
6592
|
+
ownedRoots: new Set,
|
|
6593
|
+
localNames: new Set
|
|
5918
6594
|
});
|
|
5919
6595
|
};
|
|
5920
6596
|
for (const node of record.tree.rootNode.namedChildren) {
|
|
@@ -5931,7 +6607,7 @@ function topLevelFunctionRecords(record) {
|
|
|
5931
6607
|
continue;
|
|
5932
6608
|
}
|
|
5933
6609
|
if (node.type === "variable_declaration") {
|
|
5934
|
-
const { names, expressions } =
|
|
6610
|
+
const { names, expressions } = declarationParts3(node);
|
|
5935
6611
|
for (let index = 0;index < names.length; index += 1) {
|
|
5936
6612
|
const expression = expressions[index] ?? expressions[0];
|
|
5937
6613
|
if (expression?.type !== "function_definition")
|
|
@@ -5983,7 +6659,7 @@ function moduleLevelInstanceAliases(record, summariesByModuleId) {
|
|
|
5983
6659
|
for (const node of record.tree.rootNode.namedChildren) {
|
|
5984
6660
|
if (node.type !== "variable_declaration")
|
|
5985
6661
|
continue;
|
|
5986
|
-
const { names, expressions } =
|
|
6662
|
+
const { names, expressions } = declarationParts3(node);
|
|
5987
6663
|
for (let index = 0;index < names.length; index += 1) {
|
|
5988
6664
|
const expression = expressions[index] ?? expressions[0];
|
|
5989
6665
|
if (expression?.type !== "function_call")
|
|
@@ -6002,34 +6678,50 @@ function moduleLevelInstanceAliases(record, summariesByModuleId) {
|
|
|
6002
6678
|
}
|
|
6003
6679
|
function analyzeFunction(fn, record, localFunctions, memberFunctions, moduleSummaries, moduleInstances, exportedFunctions) {
|
|
6004
6680
|
const parameters = new Set(fn.parameters);
|
|
6005
|
-
|
|
6681
|
+
const parameterOrigins = new Map(fn.parameters.map((name, index) => [name, index]));
|
|
6682
|
+
if (fn.method) {
|
|
6006
6683
|
parameters.add("self");
|
|
6684
|
+
parameterOrigins.set("self", -1);
|
|
6685
|
+
}
|
|
6007
6686
|
const locals = new Set;
|
|
6008
6687
|
const owned = new Set;
|
|
6009
6688
|
const externalAliases = new Set;
|
|
6689
|
+
const refLocals = new Set;
|
|
6010
6690
|
const instanceAliases = new Map(moduleInstances);
|
|
6011
6691
|
const nodes = directNodes(fn.body);
|
|
6012
6692
|
for (const node of nodes) {
|
|
6693
|
+
if (node.type === "function_declaration" && node.id !== fn.node.id) {
|
|
6694
|
+
const declared = node.childForFieldName("name")?.text.replace(/\s+/g, "") ?? "";
|
|
6695
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(declared))
|
|
6696
|
+
locals.add(declared);
|
|
6697
|
+
continue;
|
|
6698
|
+
}
|
|
6013
6699
|
if (node.type !== "variable_declaration")
|
|
6014
6700
|
continue;
|
|
6015
|
-
const { names, expressions } =
|
|
6701
|
+
const { names, expressions } = declarationParts3(node);
|
|
6016
6702
|
for (let index = 0;index < names.length; index += 1) {
|
|
6017
6703
|
const name = names[index];
|
|
6018
6704
|
const expression = expressions[index] ?? expressions[0];
|
|
6019
6705
|
locals.add(name);
|
|
6020
6706
|
if (!expression)
|
|
6021
6707
|
continue;
|
|
6022
|
-
|
|
6708
|
+
const createsOwnedValue = expressionCreatesOwnedValue(expression, owned);
|
|
6709
|
+
if (createsOwnedValue)
|
|
6023
6710
|
owned.add(name);
|
|
6024
6711
|
const root = rootIdentifier2(expression);
|
|
6025
|
-
|
|
6712
|
+
const parameterOrigin = root ? parameterOrigins.get(root) : undefined;
|
|
6713
|
+
if (!createsOwnedValue && root && parameterOrigin !== undefined) {
|
|
6714
|
+
parameterOrigins.set(name, parameterOrigin);
|
|
6715
|
+
} else if (!createsOwnedValue && root && (externalAliases.has(root) || !locals.has(root) && root !== name)) {
|
|
6026
6716
|
externalAliases.add(name);
|
|
6027
6717
|
} else if (root && owned.has(root)) {
|
|
6028
6718
|
owned.add(name);
|
|
6029
6719
|
}
|
|
6030
6720
|
if (expression.type === "function_call") {
|
|
6031
|
-
const path5 = callPath(expression);
|
|
6032
|
-
|
|
6721
|
+
const path5 = callPath(expression)?.replace(/\s+/g, "") ?? "";
|
|
6722
|
+
if (path5 === "React.useRef")
|
|
6723
|
+
refLocals.add(name);
|
|
6724
|
+
const match = path5.match(/^([A-Za-z_][A-Za-z0-9_]*)[.:]([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
6033
6725
|
if (match) {
|
|
6034
6726
|
const moduleId = record.imports.get(match[1]);
|
|
6035
6727
|
const summary = moduleId ? moduleSummaries.get(moduleId) : null;
|
|
@@ -6039,6 +6731,10 @@ function analyzeFunction(fn, record, localFunctions, memberFunctions, moduleSumm
|
|
|
6039
6731
|
}
|
|
6040
6732
|
}
|
|
6041
6733
|
}
|
|
6734
|
+
fn.parameterOrigins = parameterOrigins;
|
|
6735
|
+
fn.externalRoots = externalAliases;
|
|
6736
|
+
fn.ownedRoots = owned;
|
|
6737
|
+
fn.localNames = locals;
|
|
6042
6738
|
for (const node of nodes) {
|
|
6043
6739
|
if (node.type === "assignment_statement" && node.parent?.type !== "variable_declaration") {
|
|
6044
6740
|
const { left } = assignmentSides(node);
|
|
@@ -6053,7 +6749,17 @@ function analyzeFunction(fn, record, localFunctions, memberFunctions, moduleSumm
|
|
|
6053
6749
|
continue;
|
|
6054
6750
|
if (owned.has(root))
|
|
6055
6751
|
continue;
|
|
6056
|
-
if (
|
|
6752
|
+
if (refLocals.has(root) && target.text.replace(/\s+/g, "") === `${root}.current`)
|
|
6753
|
+
continue;
|
|
6754
|
+
const parameterOrigin = parameterOrigins.get(root);
|
|
6755
|
+
if (parameterOrigin !== undefined) {
|
|
6756
|
+
if (parameterOrigin === -1)
|
|
6757
|
+
fn.mutatesReceiver = true;
|
|
6758
|
+
else
|
|
6759
|
+
fn.mutatedParameterIndexes.add(parameterOrigin);
|
|
6760
|
+
continue;
|
|
6761
|
+
}
|
|
6762
|
+
if (externalAliases.has(root) || !locals.has(root))
|
|
6057
6763
|
fn.directEffect = true;
|
|
6058
6764
|
}
|
|
6059
6765
|
continue;
|
|
@@ -6063,39 +6769,174 @@ function analyzeFunction(fn, record, localFunctions, memberFunctions, moduleSumm
|
|
|
6063
6769
|
const path5 = callPath(node);
|
|
6064
6770
|
if (!path5)
|
|
6065
6771
|
continue;
|
|
6066
|
-
const
|
|
6067
|
-
|
|
6772
|
+
const arguments_ = callArguments(node);
|
|
6773
|
+
for (const index of builtinMutatedArgumentIndexes(path5, arguments_.length)) {
|
|
6774
|
+
markMutationThroughRoot(fn, rootIdentifier2(arguments_[index]));
|
|
6775
|
+
}
|
|
6776
|
+
const localTarget = !locals.has(path5) && !parameters.has(path5) ? localFunctions.get(path5) : null;
|
|
6777
|
+
if (localTarget) {
|
|
6068
6778
|
fn.dependencies.add(localTarget);
|
|
6069
|
-
|
|
6779
|
+
fn.mutationCalls.push({
|
|
6780
|
+
targetId: localTarget,
|
|
6781
|
+
receiverRoot: null,
|
|
6782
|
+
argumentRoots: arguments_.map((argument) => rootIdentifier2(argument))
|
|
6783
|
+
});
|
|
6784
|
+
}
|
|
6785
|
+
const sameMember = path5.match(/^([A-Za-z_][A-Za-z0-9_]*)([:.])([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
6070
6786
|
if (sameMember) {
|
|
6071
|
-
const receiver =
|
|
6072
|
-
if (receiver === "self" || receiver === record.exportName) {
|
|
6073
|
-
const target = memberFunctions.get(sameMember[
|
|
6074
|
-
if (target)
|
|
6787
|
+
const receiver = sameMember[1];
|
|
6788
|
+
if (receiver === "self" || receiver === record.exportName && !locals.has(receiver) && !parameters.has(receiver)) {
|
|
6789
|
+
const target = memberFunctions.get(sameMember[3]);
|
|
6790
|
+
if (target) {
|
|
6075
6791
|
fn.dependencies.add(target);
|
|
6792
|
+
fn.mutationCalls.push({
|
|
6793
|
+
targetId: target,
|
|
6794
|
+
receiverRoot: sameMember[2] === ":" ? receiver : null,
|
|
6795
|
+
argumentRoots: arguments_.map((argument) => rootIdentifier2(argument))
|
|
6796
|
+
});
|
|
6797
|
+
}
|
|
6076
6798
|
}
|
|
6077
6799
|
}
|
|
6078
|
-
const importedMember = path5.match(/^([A-Za-z_][A-Za-z0-9_]*)[.:]([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
6800
|
+
const importedMember = path5.match(/^([A-Za-z_][A-Za-z0-9_]*)([.:])([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
6079
6801
|
if (importedMember) {
|
|
6080
|
-
const
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6802
|
+
const root = importedMember[1];
|
|
6803
|
+
const moduleId = instanceAliases.get(root) ?? (!locals.has(root) && !parameters.has(root) ? record.imports.get(root) : undefined);
|
|
6804
|
+
if (moduleId) {
|
|
6805
|
+
const target = `${moduleId}::member:${importedMember[3]}`;
|
|
6806
|
+
fn.dependencies.add(target);
|
|
6807
|
+
fn.mutationCalls.push({
|
|
6808
|
+
targetId: target,
|
|
6809
|
+
receiverRoot: importedMember[2] === ":" ? root : null,
|
|
6810
|
+
argumentRoots: arguments_.map((argument) => rootIdentifier2(argument))
|
|
6811
|
+
});
|
|
6812
|
+
}
|
|
6813
|
+
} else if (!locals.has(path5) && !parameters.has(path5)) {
|
|
6084
6814
|
const moduleId = record.imports.get(path5);
|
|
6085
6815
|
const target = moduleId ? exportedFunctions.get(moduleId) : null;
|
|
6086
|
-
if (target)
|
|
6816
|
+
if (target) {
|
|
6087
6817
|
fn.dependencies.add(target);
|
|
6818
|
+
fn.mutationCalls.push({
|
|
6819
|
+
targetId: target,
|
|
6820
|
+
receiverRoot: null,
|
|
6821
|
+
argumentRoots: arguments_.map((argument) => rootIdentifier2(argument))
|
|
6822
|
+
});
|
|
6823
|
+
}
|
|
6824
|
+
}
|
|
6825
|
+
}
|
|
6826
|
+
}
|
|
6827
|
+
function markMutationThroughRoot(fn, root) {
|
|
6828
|
+
if (!root)
|
|
6829
|
+
return false;
|
|
6830
|
+
const parameterOrigin = fn.parameterOrigins.get(root);
|
|
6831
|
+
if (parameterOrigin !== undefined) {
|
|
6832
|
+
if (parameterOrigin === -1) {
|
|
6833
|
+
if (fn.mutatesReceiver)
|
|
6834
|
+
return false;
|
|
6835
|
+
fn.mutatesReceiver = true;
|
|
6836
|
+
return true;
|
|
6837
|
+
}
|
|
6838
|
+
if (fn.mutatedParameterIndexes.has(parameterOrigin))
|
|
6839
|
+
return false;
|
|
6840
|
+
fn.mutatedParameterIndexes.add(parameterOrigin);
|
|
6841
|
+
return true;
|
|
6842
|
+
}
|
|
6843
|
+
if (fn.ownedRoots.has(root))
|
|
6844
|
+
return false;
|
|
6845
|
+
if (fn.externalRoots.has(root) || !fn.localNames.has(root)) {
|
|
6846
|
+
if (fn.directEffect)
|
|
6847
|
+
return false;
|
|
6848
|
+
fn.directEffect = true;
|
|
6849
|
+
return true;
|
|
6850
|
+
}
|
|
6851
|
+
if (fn.localNames.has(root))
|
|
6852
|
+
return false;
|
|
6853
|
+
return false;
|
|
6854
|
+
}
|
|
6855
|
+
function propagateLocalMutationEffects(functions) {
|
|
6856
|
+
const byId = new Map(functions.map((fn) => [fn.id, fn]));
|
|
6857
|
+
let changed = true;
|
|
6858
|
+
while (changed) {
|
|
6859
|
+
changed = false;
|
|
6860
|
+
for (const fn of functions) {
|
|
6861
|
+
for (const call of fn.mutationCalls) {
|
|
6862
|
+
const target = byId.get(call.targetId);
|
|
6863
|
+
if (!target)
|
|
6864
|
+
continue;
|
|
6865
|
+
if (target.mutatesReceiver && markMutationThroughRoot(fn, call.receiverRoot))
|
|
6866
|
+
changed = true;
|
|
6867
|
+
for (const index of target.mutatedParameterIndexes) {
|
|
6868
|
+
if (markMutationThroughRoot(fn, call.argumentRoots[index] ?? null))
|
|
6869
|
+
changed = true;
|
|
6870
|
+
}
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
function mutationOriginForRoot(fn, root) {
|
|
6876
|
+
if (!root)
|
|
6877
|
+
return null;
|
|
6878
|
+
const parameterOrigin = fn.parameterOrigins.get(root);
|
|
6879
|
+
if (parameterOrigin === -1)
|
|
6880
|
+
return { kind: "receiver" };
|
|
6881
|
+
if (parameterOrigin !== undefined)
|
|
6882
|
+
return { kind: "parameter", index: parameterOrigin };
|
|
6883
|
+
if (fn.ownedRoots.has(root) || fn.localNames.has(root))
|
|
6884
|
+
return { kind: "local" };
|
|
6885
|
+
if (fn.externalRoots.has(root) || !fn.localNames.has(root))
|
|
6886
|
+
return { kind: "external" };
|
|
6887
|
+
return null;
|
|
6888
|
+
}
|
|
6889
|
+
function cachedMutationCalls(fn) {
|
|
6890
|
+
return fn.mutationCalls.map((call) => ({
|
|
6891
|
+
targetId: call.targetId,
|
|
6892
|
+
receiverOrigin: mutationOriginForRoot(fn, call.receiverRoot),
|
|
6893
|
+
argumentOrigins: call.argumentRoots.map((root) => mutationOriginForRoot(fn, root))
|
|
6894
|
+
}));
|
|
6895
|
+
}
|
|
6896
|
+
function applyCachedMutationOrigin(fn, origin) {
|
|
6897
|
+
if (!origin || origin.kind === "local")
|
|
6898
|
+
return false;
|
|
6899
|
+
if (origin.kind === "external") {
|
|
6900
|
+
if (fn.directEffect)
|
|
6901
|
+
return false;
|
|
6902
|
+
fn.directEffect = true;
|
|
6903
|
+
return true;
|
|
6904
|
+
}
|
|
6905
|
+
if (origin.kind === "receiver") {
|
|
6906
|
+
if (fn.mutatesReceiver)
|
|
6907
|
+
return false;
|
|
6908
|
+
fn.mutatesReceiver = true;
|
|
6909
|
+
return true;
|
|
6910
|
+
}
|
|
6911
|
+
if (fn.mutatedParameterIndexes.includes(origin.index))
|
|
6912
|
+
return false;
|
|
6913
|
+
fn.mutatedParameterIndexes.push(origin.index);
|
|
6914
|
+
fn.mutatedParameterIndexes.sort((a, b) => a - b);
|
|
6915
|
+
return true;
|
|
6916
|
+
}
|
|
6917
|
+
function propagateMutationEffects(functions) {
|
|
6918
|
+
const byId = new Map(functions.map((fn) => [fn.id, fn]));
|
|
6919
|
+
let changed = true;
|
|
6920
|
+
while (changed) {
|
|
6921
|
+
changed = false;
|
|
6922
|
+
for (const fn of functions) {
|
|
6923
|
+
for (const call of fn.mutationCalls) {
|
|
6924
|
+
const target = byId.get(call.targetId);
|
|
6925
|
+
if (!target)
|
|
6926
|
+
continue;
|
|
6927
|
+
if (target.mutatesReceiver && applyCachedMutationOrigin(fn, call.receiverOrigin))
|
|
6928
|
+
changed = true;
|
|
6929
|
+
for (const index of target.mutatedParameterIndexes) {
|
|
6930
|
+
if (applyCachedMutationOrigin(fn, call.argumentOrigins[index] ?? null))
|
|
6931
|
+
changed = true;
|
|
6932
|
+
}
|
|
6933
|
+
}
|
|
6088
6934
|
}
|
|
6089
6935
|
}
|
|
6090
6936
|
}
|
|
6091
6937
|
async function indexEffectModuleForWorker(input, moduleAliases) {
|
|
6092
6938
|
const tree = await parseLuau(input.source);
|
|
6093
|
-
const imports =
|
|
6094
|
-
for (const match of input.source.matchAll(/\blocal\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*require\s*\((.*?)\)/gs)) {
|
|
6095
|
-
const moduleId = resolveModuleReference(normalizeRequireTarget(match[2]), moduleAliases);
|
|
6096
|
-
if (moduleId)
|
|
6097
|
-
imports.set(match[1], moduleId);
|
|
6098
|
-
}
|
|
6939
|
+
const imports = topLevelImports2(tree.rootNode, moduleAliases);
|
|
6099
6940
|
const record = {
|
|
6100
6941
|
id: input.id,
|
|
6101
6942
|
keys: input.keys,
|
|
@@ -6136,6 +6977,7 @@ function analyzeEffectModuleForWorker(state, moduleSummaries, exportedFunctions)
|
|
|
6136
6977
|
for (const fn of state.functions) {
|
|
6137
6978
|
analyzeFunction(fn, state.record, localFunctions, memberFunctions, moduleSummaries, moduleInstances, exportedFunctions);
|
|
6138
6979
|
}
|
|
6980
|
+
propagateLocalMutationEffects(state.functions);
|
|
6139
6981
|
return {
|
|
6140
6982
|
id: state.record.id,
|
|
6141
6983
|
functions: state.functions.map((fn) => ({
|
|
@@ -6145,7 +6987,10 @@ function analyzeEffectModuleForWorker(state, moduleSummaries, exportedFunctions)
|
|
|
6145
6987
|
memberName: fn.memberName,
|
|
6146
6988
|
exported: fn.exported,
|
|
6147
6989
|
directEffect: fn.directEffect,
|
|
6148
|
-
|
|
6990
|
+
mutatesReceiver: fn.mutatesReceiver,
|
|
6991
|
+
mutatedParameterIndexes: [...fn.mutatedParameterIndexes].sort((a, b) => a - b),
|
|
6992
|
+
dependencies: [...fn.dependencies].sort(),
|
|
6993
|
+
mutationCalls: cachedMutationCalls(fn)
|
|
6149
6994
|
}))
|
|
6150
6995
|
};
|
|
6151
6996
|
}
|
|
@@ -6222,6 +7067,10 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
6222
7067
|
summariesByModuleId.set(identity.id, {
|
|
6223
7068
|
effectfulMembers: new Set,
|
|
6224
7069
|
effectfulExport: false,
|
|
7070
|
+
mutatingMembers: new Set,
|
|
7071
|
+
mutatingExportParameters: new Set,
|
|
7072
|
+
mutatingMemberParameters: new Map,
|
|
7073
|
+
localMutatingParameters: new Map,
|
|
6225
7074
|
instanceFactories: new Set(indexed.instanceFactories)
|
|
6226
7075
|
});
|
|
6227
7076
|
if (indexed.exportedFunctionId)
|
|
@@ -6234,6 +7083,10 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
6234
7083
|
summariesByModuleId.set(identity.id, {
|
|
6235
7084
|
effectfulMembers: new Set,
|
|
6236
7085
|
effectfulExport: false,
|
|
7086
|
+
mutatingMembers: new Set,
|
|
7087
|
+
mutatingExportParameters: new Set,
|
|
7088
|
+
mutatingMemberParameters: new Map,
|
|
7089
|
+
localMutatingParameters: new Map,
|
|
6237
7090
|
instanceFactories: new Set(cached?.instanceFactories ?? [])
|
|
6238
7091
|
});
|
|
6239
7092
|
const exported = functions2.find((fn) => fn.exported && !fn.memberName);
|
|
@@ -6269,12 +7122,7 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
6269
7122
|
const source = candidate.source ?? fs3.readFileSync(candidate.absolutePath, "utf8");
|
|
6270
7123
|
const tree = await parseLuau(source);
|
|
6271
7124
|
parseCache?.set(relativePath, { source, tree });
|
|
6272
|
-
const imports =
|
|
6273
|
-
for (const match of source.matchAll(/\blocal\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*require\s*\((.*?)\)/gs)) {
|
|
6274
|
-
const moduleId = resolveModuleReference(normalizeRequireTarget(match[2]), moduleAliases);
|
|
6275
|
-
if (moduleId)
|
|
6276
|
-
imports.set(match[1], moduleId);
|
|
6277
|
-
}
|
|
7125
|
+
const imports = topLevelImports2(tree.rootNode, moduleAliases);
|
|
6278
7126
|
rawRecords.set(identity.id, {
|
|
6279
7127
|
id: identity.id,
|
|
6280
7128
|
keys: identity.keys,
|
|
@@ -6299,6 +7147,10 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
6299
7147
|
const summary = {
|
|
6300
7148
|
effectfulMembers: new Set,
|
|
6301
7149
|
effectfulExport: false,
|
|
7150
|
+
mutatingMembers: new Set,
|
|
7151
|
+
mutatingExportParameters: new Set,
|
|
7152
|
+
mutatingMemberParameters: new Map,
|
|
7153
|
+
localMutatingParameters: new Map,
|
|
6302
7154
|
instanceFactories: new Set
|
|
6303
7155
|
};
|
|
6304
7156
|
for (const fn of functions2) {
|
|
@@ -6313,6 +7165,10 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
6313
7165
|
summariesByModuleId.set(identity.id, {
|
|
6314
7166
|
effectfulMembers: new Set,
|
|
6315
7167
|
effectfulExport: false,
|
|
7168
|
+
mutatingMembers: new Set,
|
|
7169
|
+
mutatingExportParameters: new Set,
|
|
7170
|
+
mutatingMemberParameters: new Map,
|
|
7171
|
+
localMutatingParameters: new Map,
|
|
6316
7172
|
instanceFactories: new Set(cached?.instanceFactories ?? [])
|
|
6317
7173
|
});
|
|
6318
7174
|
}
|
|
@@ -6354,6 +7210,7 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
6354
7210
|
for (const fn of functions2) {
|
|
6355
7211
|
analyzeFunction(fn, record, localFunctions, memberFunctions, summariesByModuleId, moduleInstances, exportedFunctions);
|
|
6356
7212
|
}
|
|
7213
|
+
propagateLocalMutationEffects(functions2);
|
|
6357
7214
|
analyzedFunctionStatesByModule.set(moduleId, functions2.map((fn) => ({
|
|
6358
7215
|
id: fn.id,
|
|
6359
7216
|
moduleId: fn.moduleId,
|
|
@@ -6361,7 +7218,10 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
6361
7218
|
memberName: fn.memberName,
|
|
6362
7219
|
exported: fn.exported,
|
|
6363
7220
|
directEffect: fn.directEffect,
|
|
6364
|
-
|
|
7221
|
+
mutatesReceiver: fn.mutatesReceiver,
|
|
7222
|
+
mutatedParameterIndexes: [...fn.mutatedParameterIndexes].sort((a, b) => a - b),
|
|
7223
|
+
dependencies: [...fn.dependencies].sort(),
|
|
7224
|
+
mutationCalls: cachedMutationCalls(fn)
|
|
6365
7225
|
})));
|
|
6366
7226
|
analyzedModuleCount += 1;
|
|
6367
7227
|
if ((analyzedModuleCount & 31) === 0 || analyzedModuleCount === rawRecords.size) {
|
|
@@ -6375,7 +7235,13 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
6375
7235
|
const cacheModulesResult = {};
|
|
6376
7236
|
let assembledCount = 0;
|
|
6377
7237
|
for (const identity of identities) {
|
|
6378
|
-
const states = (analyzedFunctionStatesByModule.get(identity.id) ?? cachedFunctionStates.get(identity.id) ?? []).map((fn) => ({
|
|
7238
|
+
const states = (analyzedFunctionStatesByModule.get(identity.id) ?? cachedFunctionStates.get(identity.id) ?? []).map((fn) => ({
|
|
7239
|
+
...fn,
|
|
7240
|
+
mutatesReceiver: fn.mutatesReceiver ?? false,
|
|
7241
|
+
mutatedParameterIndexes: [...fn.mutatedParameterIndexes ?? []],
|
|
7242
|
+
dependencies: [...fn.dependencies],
|
|
7243
|
+
mutationCalls: [...fn.mutationCalls ?? []]
|
|
7244
|
+
}));
|
|
6379
7245
|
for (const fn of states)
|
|
6380
7246
|
functionStates.set(fn.id, fn);
|
|
6381
7247
|
const summary = summariesByModuleId.get(identity.id);
|
|
@@ -6392,6 +7258,7 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
6392
7258
|
onProgress?.({ phase: "assemble-graph", current: assembledCount, total: graphAssemblyTotal });
|
|
6393
7259
|
}
|
|
6394
7260
|
}
|
|
7261
|
+
propagateMutationEffects([...functionStates.values()]);
|
|
6395
7262
|
const reverseDependencies = new Map;
|
|
6396
7263
|
const functions = [...functionStates.values()];
|
|
6397
7264
|
onProgress?.({ phase: "resolve", current: 0, total: functions.length });
|
|
@@ -6438,17 +7305,33 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
6438
7305
|
for (const summary of summariesByModuleId.values()) {
|
|
6439
7306
|
summary.effectfulMembers.clear();
|
|
6440
7307
|
summary.effectfulExport = false;
|
|
7308
|
+
summary.mutatingMembers.clear();
|
|
7309
|
+
summary.mutatingExportParameters.clear();
|
|
7310
|
+
summary.mutatingMemberParameters.clear();
|
|
7311
|
+
summary.localMutatingParameters.clear();
|
|
6441
7312
|
}
|
|
6442
7313
|
for (let index = 0;index < functions.length; index += 1) {
|
|
6443
7314
|
const fn = functions[index];
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
summary.
|
|
7315
|
+
const summary = summariesByModuleId.get(fn.moduleId);
|
|
7316
|
+
if (summary && fn.memberName && fn.mutatesReceiver)
|
|
7317
|
+
summary.mutatingMembers.add(fn.memberName);
|
|
7318
|
+
if (summary && fn.mutatedParameterIndexes.length > 0) {
|
|
7319
|
+
if (fn.exported && !fn.memberName) {
|
|
7320
|
+
for (const parameterIndex of fn.mutatedParameterIndexes)
|
|
7321
|
+
summary.mutatingExportParameters.add(parameterIndex);
|
|
7322
|
+
}
|
|
7323
|
+
if (fn.memberName) {
|
|
7324
|
+
summary.mutatingMemberParameters.set(fn.memberName, new Set(fn.mutatedParameterIndexes));
|
|
6451
7325
|
}
|
|
7326
|
+
if (fn.localName) {
|
|
7327
|
+
summary.localMutatingParameters.set(fn.localName, new Set(fn.mutatedParameterIndexes));
|
|
7328
|
+
}
|
|
7329
|
+
}
|
|
7330
|
+
if (summary && effectful.has(fn.id)) {
|
|
7331
|
+
if (fn.memberName)
|
|
7332
|
+
summary.effectfulMembers.add(fn.memberName);
|
|
7333
|
+
if (fn.exported)
|
|
7334
|
+
summary.effectfulExport = true;
|
|
6452
7335
|
}
|
|
6453
7336
|
if ((index & 127) === 127 || index + 1 === functions.length) {
|
|
6454
7337
|
onProgress?.({ phase: "summarize", current: index + 1, total: summarizeTotal });
|
|
@@ -6506,5 +7389,5 @@ parentPort.on("message", async (request) => {
|
|
|
6506
7389
|
}
|
|
6507
7390
|
});
|
|
6508
7391
|
|
|
6509
|
-
//# debugId=
|
|
7392
|
+
//# debugId=D3DA65AABFE680C664756E2164756E21
|
|
6510
7393
|
//# sourceMappingURL=scan-worker.js.map
|