@colrealpro/react-luau-doctor 0.17.6 → 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 +1137 -248
- package/dist/cli.js.map +17 -16
- 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/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ var package_default = {
|
|
|
11
11
|
publishConfig: {
|
|
12
12
|
access: "public"
|
|
13
13
|
},
|
|
14
|
-
version: "0.
|
|
14
|
+
version: "0.18.0",
|
|
15
15
|
description: "Static analysis for React-Luau hooks, effects, rendering, and performance.",
|
|
16
16
|
license: "MIT",
|
|
17
17
|
type: "module",
|
|
@@ -23,8 +23,7 @@ var package_default = {
|
|
|
23
23
|
"vendor",
|
|
24
24
|
"README.md",
|
|
25
25
|
"LICENSE",
|
|
26
|
-
"THIRD_PARTY_NOTICES.md"
|
|
27
|
-
"docs"
|
|
26
|
+
"THIRD_PARTY_NOTICES.md"
|
|
28
27
|
],
|
|
29
28
|
scripts: {
|
|
30
29
|
build: "bun run scripts/build.ts",
|
|
@@ -996,6 +995,10 @@ function serializeSourceEffect(summary) {
|
|
|
996
995
|
return {
|
|
997
996
|
effectfulMembers: [...summary.effectfulMembers].sort(),
|
|
998
997
|
effectfulExport: summary.effectfulExport,
|
|
998
|
+
mutatingMembers: [...summary.mutatingMembers].sort(),
|
|
999
|
+
mutatingExportParameters: [...summary.mutatingExportParameters].sort((a, b) => a - b),
|
|
1000
|
+
mutatingMemberParameters: [...summary.mutatingMemberParameters].map(([name, indexes]) => [name, [...indexes].sort((a, b) => a - b)]).sort(([left], [right]) => left.localeCompare(right)),
|
|
1001
|
+
localMutatingParameters: [...summary.localMutatingParameters].map(([name, indexes]) => [name, [...indexes].sort((a, b) => a - b)]).sort(([left], [right]) => left.localeCompare(right)),
|
|
999
1002
|
instanceFactories: [...summary.instanceFactories].sort()
|
|
1000
1003
|
};
|
|
1001
1004
|
}
|
|
@@ -1003,6 +1006,10 @@ function deserializeSourceEffect(summary) {
|
|
|
1003
1006
|
return {
|
|
1004
1007
|
effectfulMembers: new Set(summary.effectfulMembers),
|
|
1005
1008
|
effectfulExport: summary.effectfulExport,
|
|
1009
|
+
mutatingMembers: new Set(summary.mutatingMembers ?? []),
|
|
1010
|
+
mutatingExportParameters: new Set(summary.mutatingExportParameters ?? []),
|
|
1011
|
+
mutatingMemberParameters: new Map((summary.mutatingMemberParameters ?? []).map(([name, indexes]) => [name, new Set(indexes)])),
|
|
1012
|
+
localMutatingParameters: new Map((summary.localMutatingParameters ?? []).map(([name, indexes]) => [name, new Set(indexes)])),
|
|
1006
1013
|
instanceFactories: new Set(summary.instanceFactories)
|
|
1007
1014
|
};
|
|
1008
1015
|
}
|
|
@@ -2315,16 +2322,175 @@ var INSTANCE_FACTORY_MEMBERS2 = new Set([
|
|
|
2315
2322
|
function finalCallMember2(path6) {
|
|
2316
2323
|
return path6.split(/[.:]/).at(-1) ?? path6;
|
|
2317
2324
|
}
|
|
2318
|
-
function
|
|
2325
|
+
function declarationInitializer(node, name) {
|
|
2326
|
+
if (node.type !== "variable_declaration")
|
|
2327
|
+
return;
|
|
2328
|
+
const assignment = node.namedChildren.find((child) => child.type === "assignment_statement");
|
|
2329
|
+
const variables = assignment?.namedChildren.find((child) => child.type === "variable_list") ?? node.namedChildren.find((child) => child.type === "variable_list");
|
|
2330
|
+
const expressions = assignment?.namedChildren.find((child) => child.type === "expression_list");
|
|
2331
|
+
const names = variables?.namedChildren.filter((child) => child.type === "identifier") ?? [];
|
|
2332
|
+
const index = names.findIndex((candidate) => candidate.text === name);
|
|
2333
|
+
if (index < 0)
|
|
2334
|
+
return;
|
|
2335
|
+
return expressions?.namedChildren[index] ?? expressions?.namedChildren[0] ?? null;
|
|
2336
|
+
}
|
|
2337
|
+
function functionBindsName(node, name) {
|
|
2338
|
+
if (node.type !== "function_definition" && node.type !== "function_declaration")
|
|
2339
|
+
return false;
|
|
2340
|
+
const parameters = node.childForFieldName("parameters");
|
|
2341
|
+
if (!parameters)
|
|
2342
|
+
return false;
|
|
2343
|
+
return parameters.namedChildren.some((parameter) => parameter.namedChildren.some((child) => child.type === "identifier" && child.text === name) || parameter.type === "identifier" && parameter.text === name);
|
|
2344
|
+
}
|
|
2345
|
+
function loopBindsName(node, name) {
|
|
2346
|
+
if (node.type !== "for_statement")
|
|
2347
|
+
return false;
|
|
2348
|
+
const header = node.text.split(/\bdo\b/s, 1)[0] ?? "";
|
|
2349
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2350
|
+
return new RegExp(`^\\s*for\\s+(?:${escaped}\\s*=|[^\\n]*\\b${escaped}\\b[^\\n]*\\bin\\b)`, "s").test(header);
|
|
2351
|
+
}
|
|
2352
|
+
function visibleInitializer(name, from, context) {
|
|
2353
|
+
let current = from.parent;
|
|
2354
|
+
while (current) {
|
|
2355
|
+
if (current.type === "block" || current.id === context.root.id) {
|
|
2356
|
+
const children = current.namedChildren;
|
|
2357
|
+
for (let index = children.length - 1;index >= 0; index -= 1) {
|
|
2358
|
+
const child = children[index];
|
|
2359
|
+
if (child.startIndex >= from.startIndex)
|
|
2360
|
+
continue;
|
|
2361
|
+
const initializer = declarationInitializer(child, name);
|
|
2362
|
+
if (initializer !== undefined)
|
|
2363
|
+
return initializer;
|
|
2364
|
+
if (child.type === "function_declaration") {
|
|
2365
|
+
const declared = child.childForFieldName("name")?.text.replace(/\s+/g, "") ?? "";
|
|
2366
|
+
if (declared === name)
|
|
2367
|
+
return null;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (functionBindsName(current, name) || loopBindsName(current, name))
|
|
2372
|
+
return null;
|
|
2373
|
+
current = current.parent;
|
|
2374
|
+
}
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
function expressionHasRobloxOrigin(expression, from, context, seen) {
|
|
2378
|
+
if (!expression)
|
|
2379
|
+
return false;
|
|
2380
|
+
const normalized = expression.text.replace(/\s+/g, "");
|
|
2381
|
+
if (/^(?:game|workspace|script)(?:[.:]|$)/.test(normalized) || /^Instance\.new\(/.test(normalized))
|
|
2382
|
+
return true;
|
|
2383
|
+
const root = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*)/)?.[1];
|
|
2384
|
+
if (!root || root === "require" || seen.has(root))
|
|
2385
|
+
return false;
|
|
2386
|
+
if (expression.type !== "identifier" && expression.type !== "function_call")
|
|
2387
|
+
return false;
|
|
2388
|
+
seen.add(root);
|
|
2389
|
+
return expressionHasRobloxOrigin(visibleInitializer(root, from, context), from, context, seen);
|
|
2390
|
+
}
|
|
2391
|
+
function hasRobloxReceiver(path6, call, context) {
|
|
2392
|
+
const normalized = path6.replace(/\s+/g, "");
|
|
2393
|
+
if (/^(?:game|workspace|script)(?:[.:]|$)/.test(normalized))
|
|
2394
|
+
return true;
|
|
2395
|
+
const root = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*)/)?.[1];
|
|
2396
|
+
if (!root)
|
|
2397
|
+
return false;
|
|
2398
|
+
return expressionHasRobloxOrigin(visibleInitializer(root, call, context), call, context, new Set([root]));
|
|
2399
|
+
}
|
|
2400
|
+
var runServiceCallCache = new WeakMap;
|
|
2401
|
+
var runServiceExpressionCache = new WeakMap;
|
|
2402
|
+
function cachedNodeBoolean(cache, context, node, compute) {
|
|
2403
|
+
let entries = cache.get(context);
|
|
2404
|
+
if (!entries) {
|
|
2405
|
+
entries = new Map;
|
|
2406
|
+
cache.set(context, entries);
|
|
2407
|
+
}
|
|
2408
|
+
const cached = entries.get(node.id);
|
|
2409
|
+
if (cached !== undefined)
|
|
2410
|
+
return cached;
|
|
2411
|
+
const value = compute();
|
|
2412
|
+
entries.set(node.id, value);
|
|
2413
|
+
return value;
|
|
2414
|
+
}
|
|
2415
|
+
var RUN_SERVICE_EVENTS = new Set([
|
|
2416
|
+
"RenderStepped",
|
|
2417
|
+
"Heartbeat",
|
|
2418
|
+
"Stepped",
|
|
2419
|
+
"PreRender",
|
|
2420
|
+
"PreSimulation",
|
|
2421
|
+
"PostSimulation"
|
|
2422
|
+
]);
|
|
2423
|
+
function expressionIsRunService(expression, from, context, seen) {
|
|
2424
|
+
if (!expression)
|
|
2425
|
+
return false;
|
|
2426
|
+
const normalized = expression.text.replace(/\s+/g, "");
|
|
2427
|
+
if (/^game:GetService\(["']RunService["']\)$/.test(normalized))
|
|
2428
|
+
return true;
|
|
2429
|
+
if (expression.type !== "identifier")
|
|
2430
|
+
return false;
|
|
2431
|
+
const name = expression.text;
|
|
2432
|
+
if (seen.has(name))
|
|
2433
|
+
return false;
|
|
2434
|
+
seen.add(name);
|
|
2435
|
+
return expressionIsRunService(visibleInitializer(name, from, context), from, context, seen);
|
|
2436
|
+
}
|
|
2437
|
+
function rootIsRunService(root, from, context) {
|
|
2438
|
+
if (root === "game")
|
|
2439
|
+
return false;
|
|
2440
|
+
return expressionIsRunService(visibleInitializer(root, from, context), from, context, new Set([root]));
|
|
2441
|
+
}
|
|
2442
|
+
function isHighFrequencyRunServiceExpression(node, context) {
|
|
2443
|
+
return cachedNodeBoolean(runServiceExpressionCache, context, node, () => {
|
|
2444
|
+
const normalized = node.text.replace(/\s+/g, "");
|
|
2445
|
+
if (/^game:GetService\(["']RunService["']\)\.(?:RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation)$/.test(normalized)) {
|
|
2446
|
+
return true;
|
|
2447
|
+
}
|
|
2448
|
+
const match = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*)\.(RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation)$/);
|
|
2449
|
+
return Boolean(match && RUN_SERVICE_EVENTS.has(match[2]) && rootIsRunService(match[1], node, context));
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
function isHighFrequencyRunServiceCall(call, context) {
|
|
2453
|
+
return cachedNodeBoolean(runServiceCallCache, context, call, () => {
|
|
2454
|
+
const normalized = (context.getCallPath(call) ?? "").replace(/\s+/g, "");
|
|
2455
|
+
const direct = call.text.replace(/\s+/g, "");
|
|
2456
|
+
if (/^game:GetService\(["']RunService["']\)(?:\.(?:RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation):Connect|:BindToRenderStep|:BindToSimulation)\(/.test(direct)) {
|
|
2457
|
+
return true;
|
|
2458
|
+
}
|
|
2459
|
+
const event = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*)\.(RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation):Connect$/);
|
|
2460
|
+
if (event)
|
|
2461
|
+
return rootIsRunService(event[1], call, context);
|
|
2462
|
+
const bind = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*):(?:BindToRenderStep|BindToSimulation)$/);
|
|
2463
|
+
return Boolean(bind && rootIsRunService(bind[1], call, context));
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
function sourceHasHighFrequencyRunService(source) {
|
|
2467
|
+
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)) {
|
|
2468
|
+
return true;
|
|
2469
|
+
}
|
|
2470
|
+
const aliases = new Set;
|
|
2471
|
+
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)) {
|
|
2472
|
+
aliases.add(match[1]);
|
|
2473
|
+
}
|
|
2474
|
+
for (const alias of aliases) {
|
|
2475
|
+
const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2476
|
+
const pattern = new RegExp(`\\b${escaped}\\s*(?:\\.\\s*(?:RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation)\\s*:\\s*Connect|:\\s*(?:BindToRenderStep|BindToSimulation))`);
|
|
2477
|
+
if (pattern.test(source))
|
|
2478
|
+
return true;
|
|
2479
|
+
}
|
|
2480
|
+
return false;
|
|
2481
|
+
}
|
|
2482
|
+
function knownYieldReason(path6, call, context) {
|
|
2319
2483
|
const normalized = path6.replace(/\s+/g, "");
|
|
2320
2484
|
if (normalized === "task.wait")
|
|
2321
2485
|
return "task.wait";
|
|
2322
2486
|
if (normalized === "coroutine.yield")
|
|
2323
2487
|
return "coroutine.yield";
|
|
2324
2488
|
const member = finalCallMember2(normalized);
|
|
2325
|
-
if (YIELDING_ENGINE_MEMBERS.has(member))
|
|
2326
|
-
return
|
|
2327
|
-
|
|
2489
|
+
if (!YIELDING_ENGINE_MEMBERS.has(member))
|
|
2490
|
+
return null;
|
|
2491
|
+
if (!call || !context)
|
|
2492
|
+
return null;
|
|
2493
|
+
return hasRobloxReceiver(normalized, call, context) ? member : null;
|
|
2328
2494
|
}
|
|
2329
2495
|
function functionYieldPoint(node, context) {
|
|
2330
2496
|
for (const candidate of context.walk(node)) {
|
|
@@ -2335,7 +2501,7 @@ function functionYieldPoint(node, context) {
|
|
|
2335
2501
|
if (owner && nearest !== owner)
|
|
2336
2502
|
continue;
|
|
2337
2503
|
const path6 = context.resolveCallPath(context.getCallPath(candidate) ?? "");
|
|
2338
|
-
const reason = knownYieldReason(path6);
|
|
2504
|
+
const reason = knownYieldReason(path6, candidate, context);
|
|
2339
2505
|
if (reason)
|
|
2340
2506
|
return { call: candidate, reason };
|
|
2341
2507
|
}
|
|
@@ -2536,6 +2702,38 @@ function isNameShadowedBetween(node, boundary, name) {
|
|
|
2536
2702
|
}
|
|
2537
2703
|
return false;
|
|
2538
2704
|
}
|
|
2705
|
+
var topLevelBindingsCache = new WeakMap;
|
|
2706
|
+
function topLevelBindings(boundary) {
|
|
2707
|
+
const cached = topLevelBindingsCache.get(boundary);
|
|
2708
|
+
if (cached)
|
|
2709
|
+
return cached;
|
|
2710
|
+
const result = new Map;
|
|
2711
|
+
const add = (name, node) => {
|
|
2712
|
+
const existing = result.get(name) ?? [];
|
|
2713
|
+
existing.push(node);
|
|
2714
|
+
result.set(name, existing);
|
|
2715
|
+
};
|
|
2716
|
+
for (const child of boundary.body?.namedChildren ?? []) {
|
|
2717
|
+
if (child.type === "variable_declaration") {
|
|
2718
|
+
for (const name of declarationNames2(child))
|
|
2719
|
+
add(name, child);
|
|
2720
|
+
} else if (child.type === "function_declaration") {
|
|
2721
|
+
const declared = child.childForFieldName("name")?.text.replace(/\s+/g, "") ?? "";
|
|
2722
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(declared))
|
|
2723
|
+
add(declared, child);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
topLevelBindingsCache.set(boundary, result);
|
|
2727
|
+
return result;
|
|
2728
|
+
}
|
|
2729
|
+
function isBindingShadowedBetween(node, boundary, name, declaration = null) {
|
|
2730
|
+
if (isNameShadowedBetween(node, boundary, name))
|
|
2731
|
+
return true;
|
|
2732
|
+
if (!boundary.body)
|
|
2733
|
+
return false;
|
|
2734
|
+
const afterIndex = declaration?.endIndex ?? boundary.body.startIndex - 1;
|
|
2735
|
+
return (topLevelBindings(boundary).get(name) ?? []).some((binding) => binding.startIndex > afterIndex && binding.startIndex < node.startIndex);
|
|
2736
|
+
}
|
|
2539
2737
|
function containsUnshadowedIdentifier(node, name, context, boundary) {
|
|
2540
2738
|
if (!node)
|
|
2541
2739
|
return false;
|
|
@@ -4044,6 +4242,343 @@ var noNestedComponentDefinition = {
|
|
|
4044
4242
|
}
|
|
4045
4243
|
};
|
|
4046
4244
|
|
|
4245
|
+
// src/module-resolution.ts
|
|
4246
|
+
import path6 from "path";
|
|
4247
|
+
function normalizeRelative2(value) {
|
|
4248
|
+
return value.split(path6.sep).join("/");
|
|
4249
|
+
}
|
|
4250
|
+
function moduleKeys(relativePath) {
|
|
4251
|
+
let normalized = normalizeRelative2(relativePath).replace(/\.(?:lua|luau)$/i, "");
|
|
4252
|
+
if (normalized.endsWith("/init"))
|
|
4253
|
+
normalized = normalized.slice(0, -"/init".length);
|
|
4254
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
4255
|
+
if (segments[0]?.toLowerCase() === "src")
|
|
4256
|
+
segments.shift();
|
|
4257
|
+
const keys = new Set;
|
|
4258
|
+
for (let index = 0;index < segments.length; index += 1) {
|
|
4259
|
+
const suffix = segments.slice(index).join(".").toLowerCase();
|
|
4260
|
+
if (suffix)
|
|
4261
|
+
keys.add(suffix);
|
|
4262
|
+
}
|
|
4263
|
+
return [...keys];
|
|
4264
|
+
}
|
|
4265
|
+
function normalizeRequireTarget(text) {
|
|
4266
|
+
return (text.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? []).join(".").toLowerCase();
|
|
4267
|
+
}
|
|
4268
|
+
function buildUniqueFeatureAliases(modules, featureByModuleId) {
|
|
4269
|
+
const owners = new Map;
|
|
4270
|
+
for (const module of modules) {
|
|
4271
|
+
for (const key of module.keys) {
|
|
4272
|
+
const ids = owners.get(key) ?? new Set;
|
|
4273
|
+
ids.add(module.id);
|
|
4274
|
+
owners.set(key, ids);
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
const aliases = new Map;
|
|
4278
|
+
for (const module of modules) {
|
|
4279
|
+
const value = featureByModuleId.get(module.id);
|
|
4280
|
+
if (value === undefined)
|
|
4281
|
+
continue;
|
|
4282
|
+
for (const key of module.keys) {
|
|
4283
|
+
const ids = owners.get(key);
|
|
4284
|
+
if (ids?.size === 1 && ids.has(module.id))
|
|
4285
|
+
aliases.set(key, value);
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
return aliases;
|
|
4289
|
+
}
|
|
4290
|
+
function resolveModuleReference(target, aliases) {
|
|
4291
|
+
let candidate = target;
|
|
4292
|
+
while (true) {
|
|
4293
|
+
if (aliases.has(candidate))
|
|
4294
|
+
return aliases.get(candidate);
|
|
4295
|
+
const separator = candidate.indexOf(".");
|
|
4296
|
+
if (separator < 0)
|
|
4297
|
+
return null;
|
|
4298
|
+
candidate = candidate.slice(separator + 1);
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
// src/rules/parameter-mutations.ts
|
|
4303
|
+
var analysisCache = new WeakMap;
|
|
4304
|
+
var EMPTY_INDEXES = new Set;
|
|
4305
|
+
function declarationParts(node) {
|
|
4306
|
+
if (node.type !== "variable_declaration")
|
|
4307
|
+
return { names: [], expressions: [] };
|
|
4308
|
+
const assignment = node.namedChildren.find((child) => child.type === "assignment_statement");
|
|
4309
|
+
const variables = assignment?.namedChildren.find((child) => child.type === "variable_list") ?? node.namedChildren.find((child) => child.type === "variable_list");
|
|
4310
|
+
const expressions = assignment?.namedChildren.find((child) => child.type === "expression_list");
|
|
4311
|
+
return {
|
|
4312
|
+
names: variables?.namedChildren.filter((child) => child.type === "identifier").map((child) => child.text) ?? [],
|
|
4313
|
+
expressions: expressions?.namedChildren ?? []
|
|
4314
|
+
};
|
|
4315
|
+
}
|
|
4316
|
+
function assignmentParts(node) {
|
|
4317
|
+
if (node.type !== "assignment_statement" || node.parent?.type === "variable_declaration") {
|
|
4318
|
+
return { names: [], expressions: [] };
|
|
4319
|
+
}
|
|
4320
|
+
const variables = node.namedChildren.find((child) => child.type === "variable_list");
|
|
4321
|
+
const expressions = node.namedChildren.find((child) => child.type === "expression_list");
|
|
4322
|
+
return {
|
|
4323
|
+
names: variables?.namedChildren.filter((child) => child.type === "identifier").map((child) => child.text) ?? [],
|
|
4324
|
+
expressions: expressions?.namedChildren ?? []
|
|
4325
|
+
};
|
|
4326
|
+
}
|
|
4327
|
+
function expressionForName(node, name) {
|
|
4328
|
+
const { names, expressions } = node.type === "variable_declaration" ? declarationParts(node) : assignmentParts(node);
|
|
4329
|
+
const index = names.indexOf(name);
|
|
4330
|
+
if (index < 0)
|
|
4331
|
+
return;
|
|
4332
|
+
return expressions[index] ?? expressions[0] ?? null;
|
|
4333
|
+
}
|
|
4334
|
+
function currentModuleSummary(context) {
|
|
4335
|
+
for (const key of moduleKeys(context.relativePath)) {
|
|
4336
|
+
const summary = context.project.sourceEffects.get(key);
|
|
4337
|
+
if (summary)
|
|
4338
|
+
return summary;
|
|
4339
|
+
}
|
|
4340
|
+
return null;
|
|
4341
|
+
}
|
|
4342
|
+
function topLevelImports(context) {
|
|
4343
|
+
const result = new Map;
|
|
4344
|
+
for (const node of context.root.namedChildren) {
|
|
4345
|
+
if (node.type !== "variable_declaration")
|
|
4346
|
+
continue;
|
|
4347
|
+
const { names, expressions } = declarationParts(node);
|
|
4348
|
+
for (let index = 0;index < names.length; index += 1) {
|
|
4349
|
+
const expression = expressions[index] ?? expressions[0];
|
|
4350
|
+
if (expression?.type !== "function_call")
|
|
4351
|
+
continue;
|
|
4352
|
+
const match = expression.text.match(/^\s*require\s*\((.*?)\)\s*$/s);
|
|
4353
|
+
if (!match)
|
|
4354
|
+
continue;
|
|
4355
|
+
const summary = resolveModuleReference(normalizeRequireTarget(match[1]), context.project.sourceEffects);
|
|
4356
|
+
if (summary)
|
|
4357
|
+
result.set(names[index], { declaration: node, summary });
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
return result;
|
|
4361
|
+
}
|
|
4362
|
+
function analysisFor(context) {
|
|
4363
|
+
const cached = analysisCache.get(context);
|
|
4364
|
+
if (cached)
|
|
4365
|
+
return cached;
|
|
4366
|
+
const imports = topLevelImports(context);
|
|
4367
|
+
const mutatingFactoryMembers = new Set;
|
|
4368
|
+
for (const { summary } of imports.values()) {
|
|
4369
|
+
if (summary.instanceFactories.size === 0)
|
|
4370
|
+
continue;
|
|
4371
|
+
for (const member of summary.mutatingMemberParameters.keys())
|
|
4372
|
+
mutatingFactoryMembers.add(member);
|
|
4373
|
+
}
|
|
4374
|
+
const result = {
|
|
4375
|
+
currentSummary: currentModuleSummary(context),
|
|
4376
|
+
imports,
|
|
4377
|
+
mutatingFactoryMembers,
|
|
4378
|
+
callIndexes: new Map,
|
|
4379
|
+
originCache: new Map
|
|
4380
|
+
};
|
|
4381
|
+
analysisCache.set(context, result);
|
|
4382
|
+
return result;
|
|
4383
|
+
}
|
|
4384
|
+
function directChildContaining(block, node) {
|
|
4385
|
+
for (const child of block.namedChildren) {
|
|
4386
|
+
if (child.startIndex <= node.startIndex && child.endIndex >= node.endIndex)
|
|
4387
|
+
return child;
|
|
4388
|
+
}
|
|
4389
|
+
return null;
|
|
4390
|
+
}
|
|
4391
|
+
function visibleBinding(name, node, owner) {
|
|
4392
|
+
let current = node;
|
|
4393
|
+
while (current && !sameNode(current, owner.node)) {
|
|
4394
|
+
const parent = current.parent;
|
|
4395
|
+
if (parent?.type === "block") {
|
|
4396
|
+
const containing = directChildContaining(parent, node);
|
|
4397
|
+
const beforeIndex = containing?.startIndex ?? node.startIndex;
|
|
4398
|
+
const children = parent.namedChildren;
|
|
4399
|
+
for (let index = children.length - 1;index >= 0; index -= 1) {
|
|
4400
|
+
const child = children[index];
|
|
4401
|
+
if (child.startIndex >= beforeIndex)
|
|
4402
|
+
continue;
|
|
4403
|
+
if (child.type !== "variable_declaration" && child.type !== "assignment_statement")
|
|
4404
|
+
continue;
|
|
4405
|
+
const expression = expressionForName(child, name);
|
|
4406
|
+
if (expression !== undefined)
|
|
4407
|
+
return { node: child, expression };
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
current = parent;
|
|
4411
|
+
}
|
|
4412
|
+
return null;
|
|
4413
|
+
}
|
|
4414
|
+
function isTopLevelFunctionVisible(context, name, owner) {
|
|
4415
|
+
for (const node of context.root.namedChildren) {
|
|
4416
|
+
if (node.startIndex >= owner.node.startIndex)
|
|
4417
|
+
break;
|
|
4418
|
+
if (node.type === "function_declaration") {
|
|
4419
|
+
const declared = node.childForFieldName("name")?.text.replace(/\s+/g, "") ?? "";
|
|
4420
|
+
if (declared === name)
|
|
4421
|
+
return true;
|
|
4422
|
+
continue;
|
|
4423
|
+
}
|
|
4424
|
+
if (node.type !== "variable_declaration")
|
|
4425
|
+
continue;
|
|
4426
|
+
const { names, expressions } = declarationParts(node);
|
|
4427
|
+
const index = names.indexOf(name);
|
|
4428
|
+
if (index < 0)
|
|
4429
|
+
continue;
|
|
4430
|
+
if ((expressions[index] ?? expressions[0])?.type === "function_definition")
|
|
4431
|
+
return true;
|
|
4432
|
+
}
|
|
4433
|
+
return false;
|
|
4434
|
+
}
|
|
4435
|
+
function moduleBindingVisible(binding, name, call, owner) {
|
|
4436
|
+
if (binding.declaration.startIndex >= owner.node.startIndex)
|
|
4437
|
+
return false;
|
|
4438
|
+
if (owner.parameters.includes(name))
|
|
4439
|
+
return false;
|
|
4440
|
+
return visibleBinding(name, call, owner) === null;
|
|
4441
|
+
}
|
|
4442
|
+
function builtinMutatedParameterIndexes(path7, argumentCount) {
|
|
4443
|
+
switch (path7) {
|
|
4444
|
+
case "rawset":
|
|
4445
|
+
case "setmetatable":
|
|
4446
|
+
case "table.clear":
|
|
4447
|
+
case "table.freeze":
|
|
4448
|
+
case "table.insert":
|
|
4449
|
+
case "table.remove":
|
|
4450
|
+
case "table.sort":
|
|
4451
|
+
return argumentCount > 0 ? new Set([0]) : EMPTY_INDEXES;
|
|
4452
|
+
case "table.move":
|
|
4453
|
+
return argumentCount >= 5 ? new Set([4]) : argumentCount > 0 ? new Set([0]) : EMPTY_INDEXES;
|
|
4454
|
+
default:
|
|
4455
|
+
return EMPTY_INDEXES;
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
function importedFactorySummary(context, receiver, call, owner, analysis) {
|
|
4459
|
+
const binding = visibleBinding(receiver, call, owner);
|
|
4460
|
+
if (!binding?.expression)
|
|
4461
|
+
return null;
|
|
4462
|
+
const expression = binding.expression;
|
|
4463
|
+
if (expression.type === "identifier") {
|
|
4464
|
+
return importedFactorySummary(context, expression.text, expression, owner, analysis);
|
|
4465
|
+
}
|
|
4466
|
+
if (expression.type !== "function_call")
|
|
4467
|
+
return null;
|
|
4468
|
+
const path7 = normalizeExpressionText(context.getCallPath(expression) ?? "");
|
|
4469
|
+
const match = path7.match(/^([A-Za-z_][A-Za-z0-9_]*)[.:]([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
4470
|
+
if (!match)
|
|
4471
|
+
return null;
|
|
4472
|
+
const imported = analysis.imports.get(match[1]);
|
|
4473
|
+
if (!imported || !moduleBindingVisible(imported, match[1], expression, owner))
|
|
4474
|
+
return null;
|
|
4475
|
+
return imported.summary.instanceFactories.has(match[2]) ? imported.summary : null;
|
|
4476
|
+
}
|
|
4477
|
+
function mightMutateParameters(context, call) {
|
|
4478
|
+
const path7 = normalizeExpressionText(context.getCallPath(call) ?? "");
|
|
4479
|
+
if (builtinMutatedParameterIndexes(path7, context.callArguments(call).length).size > 0)
|
|
4480
|
+
return true;
|
|
4481
|
+
const analysis = analysisFor(context);
|
|
4482
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(path7)) {
|
|
4483
|
+
if ((analysis.currentSummary?.localMutatingParameters.get(path7)?.size ?? 0) > 0)
|
|
4484
|
+
return true;
|
|
4485
|
+
return (analysis.imports.get(path7)?.summary.mutatingExportParameters.size ?? 0) > 0;
|
|
4486
|
+
}
|
|
4487
|
+
const member = path7.match(/^([A-Za-z_][A-Za-z0-9_]*)[.:]([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
4488
|
+
if (!member)
|
|
4489
|
+
return false;
|
|
4490
|
+
if ((analysis.imports.get(member[1])?.summary.mutatingMemberParameters.get(member[2])?.size ?? 0) > 0)
|
|
4491
|
+
return true;
|
|
4492
|
+
return analysis.mutatingFactoryMembers.has(member[2]);
|
|
4493
|
+
}
|
|
4494
|
+
function mutatedParameterIndexesForCall(context, call, owner) {
|
|
4495
|
+
const analysis = analysisFor(context);
|
|
4496
|
+
const cached = analysis.callIndexes.get(call.id);
|
|
4497
|
+
if (cached)
|
|
4498
|
+
return cached;
|
|
4499
|
+
const path7 = normalizeExpressionText(context.getCallPath(call) ?? "");
|
|
4500
|
+
const builtin = builtinMutatedParameterIndexes(path7, context.callArguments(call).length);
|
|
4501
|
+
if (builtin.size > 0) {
|
|
4502
|
+
analysis.callIndexes.set(call.id, builtin);
|
|
4503
|
+
return builtin;
|
|
4504
|
+
}
|
|
4505
|
+
let result = EMPTY_INDEXES;
|
|
4506
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(path7)) {
|
|
4507
|
+
const importBinding = analysis.imports.get(path7);
|
|
4508
|
+
if (importBinding && moduleBindingVisible(importBinding, path7, call, owner)) {
|
|
4509
|
+
result = importBinding.summary.mutatingExportParameters;
|
|
4510
|
+
} else if (!owner.parameters.includes(path7) && visibleBinding(path7, call, owner) === null && isTopLevelFunctionVisible(context, path7, owner)) {
|
|
4511
|
+
result = analysis.currentSummary?.localMutatingParameters.get(path7) ?? EMPTY_INDEXES;
|
|
4512
|
+
}
|
|
4513
|
+
} else {
|
|
4514
|
+
const member = path7.match(/^([A-Za-z_][A-Za-z0-9_]*)([.:])([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
4515
|
+
if (member) {
|
|
4516
|
+
const imported = analysis.imports.get(member[1]);
|
|
4517
|
+
if (imported && moduleBindingVisible(imported, member[1], call, owner)) {
|
|
4518
|
+
result = imported.summary.mutatingMemberParameters.get(member[3]) ?? EMPTY_INDEXES;
|
|
4519
|
+
} else {
|
|
4520
|
+
const factory = importedFactorySummary(context, member[1], call, owner, analysis);
|
|
4521
|
+
result = factory?.mutatingMemberParameters.get(member[3]) ?? EMPTY_INDEXES;
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
analysis.callIndexes.set(call.id, result);
|
|
4526
|
+
return result;
|
|
4527
|
+
}
|
|
4528
|
+
function freshExpression(context, expression) {
|
|
4529
|
+
if (expression.type === "table_constructor" || expression.type === "function_definition")
|
|
4530
|
+
return true;
|
|
4531
|
+
if (expression.type !== "function_call")
|
|
4532
|
+
return false;
|
|
4533
|
+
const path7 = normalizeExpressionText(context.getCallPath(expression) ?? "");
|
|
4534
|
+
if (path7 === "table.clone" || path7 === "table.create" || path7 === "table.pack")
|
|
4535
|
+
return true;
|
|
4536
|
+
if (path7 === "setmetatable") {
|
|
4537
|
+
const first = context.callArguments(expression)[0];
|
|
4538
|
+
return Boolean(first && freshExpression(context, first));
|
|
4539
|
+
}
|
|
4540
|
+
return false;
|
|
4541
|
+
}
|
|
4542
|
+
function stateBindingForName(context, owner, name, bindingNode) {
|
|
4543
|
+
return context.model.stateBindings.find((binding) => binding.owner === owner && binding.valueName === name && sameNode(binding.declaration, bindingNode)) ?? null;
|
|
4544
|
+
}
|
|
4545
|
+
function resolveNameOrigin(context, name, atNode, owner, seen) {
|
|
4546
|
+
const key = `${owner.node.id}:${name}:${atNode.startIndex}`;
|
|
4547
|
+
if (seen.has(key))
|
|
4548
|
+
return { kind: "unknown" };
|
|
4549
|
+
seen.add(key);
|
|
4550
|
+
const binding = visibleBinding(name, atNode, owner);
|
|
4551
|
+
if (binding) {
|
|
4552
|
+
const state = stateBindingForName(context, owner, name, binding.node);
|
|
4553
|
+
if (state)
|
|
4554
|
+
return { kind: "state", binding: state };
|
|
4555
|
+
if (!binding.expression)
|
|
4556
|
+
return { kind: "unknown" };
|
|
4557
|
+
return resolveExpressionOriginInternal(context, binding.expression, owner, seen);
|
|
4558
|
+
}
|
|
4559
|
+
if (owner.isComponent && owner.parameters[0] === name)
|
|
4560
|
+
return { kind: "props", name };
|
|
4561
|
+
return { kind: "unknown" };
|
|
4562
|
+
}
|
|
4563
|
+
function resolveExpressionOriginInternal(context, expression, owner, seen) {
|
|
4564
|
+
if (freshExpression(context, expression))
|
|
4565
|
+
return { kind: "fresh" };
|
|
4566
|
+
const root = rootIdentifier(expression.text);
|
|
4567
|
+
if (!root)
|
|
4568
|
+
return { kind: "unknown" };
|
|
4569
|
+
return resolveNameOrigin(context, root, expression, owner, seen);
|
|
4570
|
+
}
|
|
4571
|
+
function mutationOriginForExpression(context, expression, owner) {
|
|
4572
|
+
const analysis = analysisFor(context);
|
|
4573
|
+
const key = `${owner.node.id}:${expression.id}`;
|
|
4574
|
+
const cached = analysis.originCache.get(key);
|
|
4575
|
+
if (cached)
|
|
4576
|
+
return cached;
|
|
4577
|
+
const result = resolveExpressionOriginInternal(context, expression, owner, new Set);
|
|
4578
|
+
analysis.originCache.set(key, result);
|
|
4579
|
+
return result;
|
|
4580
|
+
}
|
|
4581
|
+
|
|
4047
4582
|
// src/rules/no-prop-mutation.ts
|
|
4048
4583
|
var noPropMutation = {
|
|
4049
4584
|
id: "react-luau/no-prop-mutation",
|
|
@@ -4053,7 +4588,27 @@ var noPropMutation = {
|
|
|
4053
4588
|
run(context) {
|
|
4054
4589
|
const diagnostics = [];
|
|
4055
4590
|
for (const node of context.walk()) {
|
|
4056
|
-
if (node.type
|
|
4591
|
+
if (node.type === "assignment_statement" || node.type === "update_statement") {
|
|
4592
|
+
const component2 = context.containingComponent(node);
|
|
4593
|
+
if (!component2 || !context.isDirectlyExecutedInFunction(node, component2))
|
|
4594
|
+
continue;
|
|
4595
|
+
const propsName2 = component2.parameters[0];
|
|
4596
|
+
if (!propsName2)
|
|
4597
|
+
continue;
|
|
4598
|
+
if (isBindingShadowedBetween(node, component2, propsName2))
|
|
4599
|
+
continue;
|
|
4600
|
+
const left = assignmentLeft(node.text);
|
|
4601
|
+
const escaped = propsName2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4602
|
+
if (!new RegExp(`^${escaped}\\s*(?:\\.|\\[)`).test(left))
|
|
4603
|
+
continue;
|
|
4604
|
+
diagnostics.push({
|
|
4605
|
+
node: assignmentTargetNode(node),
|
|
4606
|
+
message: `Component mutates ${propsName2} directly.`,
|
|
4607
|
+
help: "Treat props as immutable. Derive a local value, clone a table you own, or update state in the owner instead."
|
|
4608
|
+
});
|
|
4609
|
+
continue;
|
|
4610
|
+
}
|
|
4611
|
+
if (node.type !== "function_call" || !mightMutateParameters(context, node))
|
|
4057
4612
|
continue;
|
|
4058
4613
|
const component = context.containingComponent(node);
|
|
4059
4614
|
if (!component || !context.isDirectlyExecutedInFunction(node, component))
|
|
@@ -4061,16 +4616,27 @@ var noPropMutation = {
|
|
|
4061
4616
|
const propsName = component.parameters[0];
|
|
4062
4617
|
if (!propsName)
|
|
4063
4618
|
continue;
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
const
|
|
4068
|
-
|
|
4619
|
+
const arguments_ = context.callArguments(node);
|
|
4620
|
+
const mutatedIndexes = mutatedParameterIndexesForCall(context, node, component);
|
|
4621
|
+
let mutatedArgument = null;
|
|
4622
|
+
for (const index of mutatedIndexes) {
|
|
4623
|
+
const argument = arguments_[index];
|
|
4624
|
+
if (!argument)
|
|
4625
|
+
continue;
|
|
4626
|
+
const origin = mutationOriginForExpression(context, argument, component);
|
|
4627
|
+
if (origin.kind !== "props" || origin.name !== propsName)
|
|
4628
|
+
continue;
|
|
4629
|
+
mutatedArgument = argument;
|
|
4630
|
+
break;
|
|
4631
|
+
}
|
|
4632
|
+
if (!mutatedArgument)
|
|
4069
4633
|
continue;
|
|
4634
|
+
const path7 = context.resolveCallPath(context.getCallPath(node) ?? "") || "This call";
|
|
4070
4635
|
diagnostics.push({
|
|
4071
|
-
node:
|
|
4072
|
-
|
|
4073
|
-
|
|
4636
|
+
node: callNameNode(node),
|
|
4637
|
+
highlights: [mutatedArgument],
|
|
4638
|
+
message: `${path7} mutates an argument derived from ${propsName}.`,
|
|
4639
|
+
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."
|
|
4074
4640
|
});
|
|
4075
4641
|
}
|
|
4076
4642
|
return diagnostics;
|
|
@@ -4094,9 +4660,9 @@ function freshDescription(node, context) {
|
|
|
4094
4660
|
for (const child of context.walk(node)) {
|
|
4095
4661
|
if (child.type !== "function_call")
|
|
4096
4662
|
continue;
|
|
4097
|
-
const
|
|
4098
|
-
if (FRESH_CALLS.some((pattern) => pattern.test(
|
|
4099
|
-
return `${
|
|
4663
|
+
const path7 = context.getCallPath(child) ?? "";
|
|
4664
|
+
if (FRESH_CALLS.some((pattern) => pattern.test(path7)) || /Random\.new\s*\([^)]*\)\s*:\s*Next(?:Integer|Number)/s.test(child.text)) {
|
|
4665
|
+
return `${path7 || "fresh-value call"}()`;
|
|
4100
4666
|
}
|
|
4101
4667
|
}
|
|
4102
4668
|
return null;
|
|
@@ -4151,18 +4717,20 @@ var noSetStateInRender = {
|
|
|
4151
4717
|
for (const component of context.model.functions) {
|
|
4152
4718
|
if (!component.isComponent || !component.body)
|
|
4153
4719
|
continue;
|
|
4154
|
-
const
|
|
4720
|
+
const bindings = stateBindingsFor(context, component);
|
|
4721
|
+
const setters = new Map(bindings.map((binding) => [binding.setterName, binding]));
|
|
4155
4722
|
if (setters.size === 0)
|
|
4156
4723
|
continue;
|
|
4157
4724
|
for (const statement of component.body.namedChildren) {
|
|
4158
4725
|
if (statement.type !== "function_call")
|
|
4159
4726
|
continue;
|
|
4160
|
-
const
|
|
4161
|
-
|
|
4727
|
+
const path7 = context.getCallPath(statement);
|
|
4728
|
+
const binding = path7 ? setters.get(path7) : undefined;
|
|
4729
|
+
if (!path7 || !binding || isBindingShadowedBetween(statement, component, path7, binding.declaration))
|
|
4162
4730
|
continue;
|
|
4163
4731
|
diagnostics.push({
|
|
4164
4732
|
node: callNameNode(statement),
|
|
4165
|
-
message: `${
|
|
4733
|
+
message: `${path7}() is called unconditionally during component render.`,
|
|
4166
4734
|
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."
|
|
4167
4735
|
});
|
|
4168
4736
|
}
|
|
@@ -4197,111 +4765,53 @@ function incompleteDelimitedNode(node) {
|
|
|
4197
4765
|
if (!closer)
|
|
4198
4766
|
return null;
|
|
4199
4767
|
return node.text.trimEnd().endsWith(closer) ? null : closer;
|
|
4200
|
-
}
|
|
4201
|
-
function isBundledGrammarTypeGap(node) {
|
|
4202
|
-
let current = node;
|
|
4203
|
-
while (current) {
|
|
4204
|
-
if (TYPE_SYNTAX_ANCESTORS.has(current.type)) {
|
|
4205
|
-
if (current.type !== "type_definition")
|
|
4206
|
-
return true;
|
|
4207
|
-
if (/^\s*(?:export\s+)?type\s+[A-Za-z_][A-Za-z0-9_]*/s.test(current.text))
|
|
4208
|
-
return true;
|
|
4209
|
-
}
|
|
4210
|
-
current = current.parent;
|
|
4211
|
-
}
|
|
4212
|
-
return false;
|
|
4213
|
-
}
|
|
4214
|
-
var parseErrors = {
|
|
4215
|
-
id: "react-luau/parse-error",
|
|
4216
|
-
category: "Correctness",
|
|
4217
|
-
severity: "error",
|
|
4218
|
-
description: "Report executable Luau syntax that the bundled parser cannot form into a complete syntax tree.",
|
|
4219
|
-
run(context) {
|
|
4220
|
-
const diagnostics = [];
|
|
4221
|
-
const covered = [];
|
|
4222
|
-
for (const node of context.walk()) {
|
|
4223
|
-
const missingCloser = incompleteDelimitedNode(node);
|
|
4224
|
-
if (!node.isError && !node.isMissing && !missingCloser)
|
|
4225
|
-
continue;
|
|
4226
|
-
if (isBundledGrammarTypeGap(node))
|
|
4227
|
-
continue;
|
|
4228
|
-
if (covered.some((range) => node.startIndex >= range.start && node.endIndex <= range.end))
|
|
4229
|
-
continue;
|
|
4230
|
-
covered.push({ start: node.startIndex, end: node.endIndex });
|
|
4231
|
-
let message = "Luau syntax could not be parsed cleanly at this location.";
|
|
4232
|
-
if (node.isMissing)
|
|
4233
|
-
message = `Luau syntax is missing ${node.type}.`;
|
|
4234
|
-
else if (missingCloser)
|
|
4235
|
-
message = `Luau syntax is missing closing ${missingCloser}.`;
|
|
4236
|
-
diagnostics.push({
|
|
4237
|
-
node,
|
|
4238
|
-
message,
|
|
4239
|
-
help: "Fix the syntax error before relying on downstream React-Luau diagnostics in this file."
|
|
4240
|
-
});
|
|
4241
|
-
}
|
|
4242
|
-
return diagnostics;
|
|
4243
|
-
}
|
|
4244
|
-
};
|
|
4245
|
-
|
|
4246
|
-
// src/module-resolution.ts
|
|
4247
|
-
import path6 from "path";
|
|
4248
|
-
function normalizeRelative2(value) {
|
|
4249
|
-
return value.split(path6.sep).join("/");
|
|
4250
|
-
}
|
|
4251
|
-
function moduleKeys(relativePath) {
|
|
4252
|
-
let normalized = normalizeRelative2(relativePath).replace(/\.(?:lua|luau)$/i, "");
|
|
4253
|
-
if (normalized.endsWith("/init"))
|
|
4254
|
-
normalized = normalized.slice(0, -"/init".length);
|
|
4255
|
-
const segments = normalized.split("/").filter(Boolean);
|
|
4256
|
-
if (segments[0]?.toLowerCase() === "src")
|
|
4257
|
-
segments.shift();
|
|
4258
|
-
const keys = new Set;
|
|
4259
|
-
for (let index = 0;index < segments.length; index += 1) {
|
|
4260
|
-
const suffix = segments.slice(index).join(".").toLowerCase();
|
|
4261
|
-
if (suffix)
|
|
4262
|
-
keys.add(suffix);
|
|
4263
|
-
}
|
|
4264
|
-
return [...keys];
|
|
4265
|
-
}
|
|
4266
|
-
function normalizeRequireTarget(text) {
|
|
4267
|
-
return (text.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? []).join(".").toLowerCase();
|
|
4268
|
-
}
|
|
4269
|
-
function buildUniqueFeatureAliases(modules, featureByModuleId) {
|
|
4270
|
-
const owners = new Map;
|
|
4271
|
-
for (const module of modules) {
|
|
4272
|
-
for (const key of module.keys) {
|
|
4273
|
-
const ids = owners.get(key) ?? new Set;
|
|
4274
|
-
ids.add(module.id);
|
|
4275
|
-
owners.set(key, ids);
|
|
4276
|
-
}
|
|
4277
|
-
}
|
|
4278
|
-
const aliases = new Map;
|
|
4279
|
-
for (const module of modules) {
|
|
4280
|
-
const value = featureByModuleId.get(module.id);
|
|
4281
|
-
if (value === undefined)
|
|
4282
|
-
continue;
|
|
4283
|
-
for (const key of module.keys) {
|
|
4284
|
-
const ids = owners.get(key);
|
|
4285
|
-
if (ids?.size === 1 && ids.has(module.id))
|
|
4286
|
-
aliases.set(key, value);
|
|
4768
|
+
}
|
|
4769
|
+
function isBundledGrammarTypeGap(node) {
|
|
4770
|
+
let current = node;
|
|
4771
|
+
while (current) {
|
|
4772
|
+
if (TYPE_SYNTAX_ANCESTORS.has(current.type)) {
|
|
4773
|
+
if (current.type !== "type_definition")
|
|
4774
|
+
return true;
|
|
4775
|
+
if (/^\s*(?:export\s+)?type\s+[A-Za-z_][A-Za-z0-9_]*/s.test(current.text))
|
|
4776
|
+
return true;
|
|
4287
4777
|
}
|
|
4778
|
+
current = current.parent;
|
|
4288
4779
|
}
|
|
4289
|
-
return
|
|
4780
|
+
return false;
|
|
4290
4781
|
}
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4782
|
+
var parseErrors = {
|
|
4783
|
+
id: "react-luau/parse-error",
|
|
4784
|
+
category: "Correctness",
|
|
4785
|
+
severity: "error",
|
|
4786
|
+
description: "Report executable Luau syntax that the bundled parser cannot form into a complete syntax tree.",
|
|
4787
|
+
run(context) {
|
|
4788
|
+
const diagnostics = [];
|
|
4789
|
+
const covered = [];
|
|
4790
|
+
for (const node of context.walk()) {
|
|
4791
|
+
const missingCloser = incompleteDelimitedNode(node);
|
|
4792
|
+
if (!node.isError && !node.isMissing && !missingCloser)
|
|
4793
|
+
continue;
|
|
4794
|
+
if (isBundledGrammarTypeGap(node))
|
|
4795
|
+
continue;
|
|
4796
|
+
if (covered.some((range) => node.startIndex >= range.start && node.endIndex <= range.end))
|
|
4797
|
+
continue;
|
|
4798
|
+
covered.push({ start: node.startIndex, end: node.endIndex });
|
|
4799
|
+
let message = "Luau syntax could not be parsed cleanly at this location.";
|
|
4800
|
+
if (node.isMissing)
|
|
4801
|
+
message = `Luau syntax is missing ${node.type}.`;
|
|
4802
|
+
else if (missingCloser)
|
|
4803
|
+
message = `Luau syntax is missing closing ${missingCloser}.`;
|
|
4804
|
+
diagnostics.push({
|
|
4805
|
+
node,
|
|
4806
|
+
message,
|
|
4807
|
+
help: "Fix the syntax error before relying on downstream React-Luau diagnostics in this file."
|
|
4808
|
+
});
|
|
4809
|
+
}
|
|
4810
|
+
return diagnostics;
|
|
4300
4811
|
}
|
|
4301
|
-
}
|
|
4812
|
+
};
|
|
4302
4813
|
|
|
4303
4814
|
// src/rules/performance-rules.ts
|
|
4304
|
-
var HIGH_FREQUENCY = /(?:RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation)\s*:\s*Connect|BindToRenderStep|BindToSimulation/;
|
|
4305
4815
|
var STATIC_DISCOVERY = /(?::|\.)(GetChildren|GetDescendants)$/;
|
|
4306
4816
|
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)$/;
|
|
4307
4817
|
function declarationForCall(call) {
|
|
@@ -4339,7 +4849,7 @@ function identifierReferences(context, owner, name, declaration) {
|
|
|
4339
4849
|
continue;
|
|
4340
4850
|
if (isIdentifierPropertyName(node))
|
|
4341
4851
|
continue;
|
|
4342
|
-
if (
|
|
4852
|
+
if (isBindingShadowedBetween(node, owner, name, declaration))
|
|
4343
4853
|
continue;
|
|
4344
4854
|
result.push(node);
|
|
4345
4855
|
}
|
|
@@ -4354,17 +4864,14 @@ function nearestAncestor(node, stop, type) {
|
|
|
4354
4864
|
}
|
|
4355
4865
|
return null;
|
|
4356
4866
|
}
|
|
4357
|
-
function highFrequencyCallback(node) {
|
|
4867
|
+
function highFrequencyCallback(node, context) {
|
|
4358
4868
|
let current = node.parent;
|
|
4359
4869
|
while (current) {
|
|
4360
4870
|
if (current.type === "function_definition") {
|
|
4361
4871
|
const argumentsNode = current.parent;
|
|
4362
4872
|
const call = argumentsNode?.type === "arguments" ? argumentsNode.parent : null;
|
|
4363
|
-
if (call?.type === "function_call")
|
|
4364
|
-
|
|
4365
|
-
if (HIGH_FREQUENCY.test(name))
|
|
4366
|
-
return current;
|
|
4367
|
-
}
|
|
4873
|
+
if (call?.type === "function_call" && isHighFrequencyRunServiceCall(call, context))
|
|
4874
|
+
return current;
|
|
4368
4875
|
}
|
|
4369
4876
|
current = current.parent;
|
|
4370
4877
|
}
|
|
@@ -4502,7 +5009,7 @@ var rerenderHighFrequencyState = {
|
|
|
4502
5009
|
const diagnostics = [];
|
|
4503
5010
|
const seen = new Set;
|
|
4504
5011
|
for (const call of context.findCalls()) {
|
|
4505
|
-
const callback = highFrequencyCallback(call);
|
|
5012
|
+
const callback = highFrequencyCallback(call, context);
|
|
4506
5013
|
if (!callback)
|
|
4507
5014
|
continue;
|
|
4508
5015
|
const component = context.containingComponent(call);
|
|
@@ -4795,7 +5302,6 @@ var preferUseRefForMutableCell = {
|
|
|
4795
5302
|
};
|
|
4796
5303
|
|
|
4797
5304
|
// src/rules/prefer-binding-over-state.ts
|
|
4798
|
-
var HIGH_FREQUENCY2 = /(?:RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation)\s*:\s*Connect|BindToRenderStep|BindToSimulation/;
|
|
4799
5305
|
var EXTERNAL_CALLBACK = /(?:^|[.:])(?:Connect|Once|Subscribe|Observe|Listen|Watch)$/i;
|
|
4800
5306
|
var NON_BINDABLE_HOST_FIELDS = new Set(["ref", "key", "children"]);
|
|
4801
5307
|
function replaceWithinNode(container, target, replacement) {
|
|
@@ -4950,7 +5456,7 @@ function isReadNode(node, valueName, owner, declaration) {
|
|
|
4950
5456
|
return false;
|
|
4951
5457
|
if (isIdentifierPropertyName2(node))
|
|
4952
5458
|
return false;
|
|
4953
|
-
if (
|
|
5459
|
+
if (isBindingShadowedBetween(node, owner, valueName, declaration))
|
|
4954
5460
|
return false;
|
|
4955
5461
|
const parent = node.parent;
|
|
4956
5462
|
if (parent?.type === "variable_list" || parent?.type === "typed_identifier")
|
|
@@ -5055,9 +5561,9 @@ function stateReadsAreBindingCompatible(valueName, owner, declaration, context,
|
|
|
5055
5561
|
}
|
|
5056
5562
|
return reads > 0;
|
|
5057
5563
|
}
|
|
5058
|
-
function callbackSource(
|
|
5059
|
-
const normalized =
|
|
5060
|
-
const highFrequency =
|
|
5564
|
+
function callbackSource(call, context) {
|
|
5565
|
+
const normalized = (context.getCallPath(call) ?? "").replace(/\s+/g, "");
|
|
5566
|
+
const highFrequency = isHighFrequencyRunServiceCall(call, context);
|
|
5061
5567
|
const final = normalized.split(/[.:]/).at(-1) ?? normalized;
|
|
5062
5568
|
const external = highFrequency || EXTERNAL_CALLBACK.test(normalized) || EXTERNAL_CALLBACK.test(final);
|
|
5063
5569
|
return { highFrequency, external };
|
|
@@ -5072,7 +5578,7 @@ function importedCallbackSource(call, callbackArgument, importedCallbacks, conte
|
|
|
5072
5578
|
if (callbackIndex < 0 || !summary.callbackParameterIndexes.includes(callbackIndex))
|
|
5073
5579
|
return null;
|
|
5074
5580
|
return {
|
|
5075
|
-
highFrequency: summary.highFrequency || args.some((arg) =>
|
|
5581
|
+
highFrequency: summary.highFrequency || args.some((arg) => isHighFrequencyRunServiceExpression(arg, context)),
|
|
5076
5582
|
external: true
|
|
5077
5583
|
};
|
|
5078
5584
|
}
|
|
@@ -5084,8 +5590,7 @@ function directCallbackSource(node, importedCallbacks, context) {
|
|
|
5084
5590
|
const imported = importedCallbackSource(call, node, importedCallbacks, context);
|
|
5085
5591
|
if (imported)
|
|
5086
5592
|
return imported;
|
|
5087
|
-
const
|
|
5088
|
-
const source = callbackSource(raw);
|
|
5593
|
+
const source = callbackSource(call, context);
|
|
5089
5594
|
return source.external ? source : null;
|
|
5090
5595
|
}
|
|
5091
5596
|
function namedFunctionCallbackSource(fn, owner, importedCallbacks, context) {
|
|
@@ -5101,7 +5606,7 @@ function namedFunctionCallbackSource(fn, owner, importedCallbacks, context) {
|
|
|
5101
5606
|
if (!callbackArg)
|
|
5102
5607
|
continue;
|
|
5103
5608
|
const imported = importedCallbackSource(call, callbackArg, importedCallbacks, context);
|
|
5104
|
-
const source = imported ?? callbackSource(
|
|
5609
|
+
const source = imported ?? callbackSource(call, context);
|
|
5105
5610
|
if (!source.external)
|
|
5106
5611
|
continue;
|
|
5107
5612
|
foundExternal = true;
|
|
@@ -5584,6 +6089,26 @@ function assignmentRight(text) {
|
|
|
5584
6089
|
const match = text.match(/^(?:.*?)(?:\+=|-=|\*=|\/=|%=|\^=|\.\.=|=)\s*(.+)$/s);
|
|
5585
6090
|
return match?.[1]?.trim() ?? null;
|
|
5586
6091
|
}
|
|
6092
|
+
function refDeclaration(owner, refName, context) {
|
|
6093
|
+
if (!owner.body)
|
|
6094
|
+
return null;
|
|
6095
|
+
for (const statement of owner.body.namedChildren) {
|
|
6096
|
+
if (statement.type !== "variable_declaration")
|
|
6097
|
+
continue;
|
|
6098
|
+
const names = declarationNames2(statement);
|
|
6099
|
+
const index = names.indexOf(refName);
|
|
6100
|
+
if (index === -1)
|
|
6101
|
+
continue;
|
|
6102
|
+
const assignment = statement.namedChildren.find((child) => child.type === "assignment_statement");
|
|
6103
|
+
const expressions = assignment?.namedChildren.find((child) => child.type === "expression_list")?.namedChildren ?? [];
|
|
6104
|
+
const expression = expressions[index] ?? expressions[0];
|
|
6105
|
+
if (!expression || expression.type !== "function_call")
|
|
6106
|
+
continue;
|
|
6107
|
+
if (context.resolveCallPath(context.getCallPath(expression) ?? "") === "React.useRef")
|
|
6108
|
+
return statement;
|
|
6109
|
+
}
|
|
6110
|
+
return null;
|
|
6111
|
+
}
|
|
5587
6112
|
function refInitializer(owner, refName, context) {
|
|
5588
6113
|
if (!owner.body)
|
|
5589
6114
|
return null;
|
|
@@ -5680,6 +6205,9 @@ var noRefCurrentInRender = {
|
|
|
5680
6205
|
const refName = refRootFromTarget(target, refs);
|
|
5681
6206
|
if (!refName || isNilGuardedLazyInit(node, refName))
|
|
5682
6207
|
continue;
|
|
6208
|
+
const declaration = refDeclaration(owner, refName, context);
|
|
6209
|
+
if (isBindingShadowedBetween(node, owner, refName, declaration))
|
|
6210
|
+
continue;
|
|
5683
6211
|
const latestValueMirror = isLatestValueMirror(node, owner, refName, context);
|
|
5684
6212
|
diagnostics.push({
|
|
5685
6213
|
node: assignmentTargetNode(node),
|
|
@@ -5693,7 +6221,7 @@ var noRefCurrentInRender = {
|
|
|
5693
6221
|
};
|
|
5694
6222
|
|
|
5695
6223
|
// src/rules/render-side-effects.ts
|
|
5696
|
-
function
|
|
6224
|
+
function declarationParts2(node) {
|
|
5697
6225
|
if (node.type !== "variable_declaration")
|
|
5698
6226
|
return { names: [], expressions: [] };
|
|
5699
6227
|
const assignment = node.namedChildren.find((child) => child.type === "assignment_statement");
|
|
@@ -5704,17 +6232,103 @@ function declarationParts(node) {
|
|
|
5704
6232
|
expressions: expressions?.namedChildren ?? []
|
|
5705
6233
|
};
|
|
5706
6234
|
}
|
|
6235
|
+
function assignmentParts2(node) {
|
|
6236
|
+
if (node.type !== "assignment_statement" || node.parent?.type === "variable_declaration")
|
|
6237
|
+
return { names: [], expressions: [] };
|
|
6238
|
+
const variables = node.namedChildren.find((child) => child.type === "variable_list");
|
|
6239
|
+
const expressions = node.namedChildren.find((child) => child.type === "expression_list");
|
|
6240
|
+
return {
|
|
6241
|
+
names: variables?.namedChildren.filter((child) => child.type === "identifier").map((child) => child.text) ?? [],
|
|
6242
|
+
expressions: expressions?.namedChildren ?? []
|
|
6243
|
+
};
|
|
6244
|
+
}
|
|
6245
|
+
function bindingParts(node) {
|
|
6246
|
+
return node.type === "variable_declaration" ? declarationParts2(node) : assignmentParts2(node);
|
|
6247
|
+
}
|
|
6248
|
+
function addSourceBinding(bindings, name, declaration, value) {
|
|
6249
|
+
const existing = bindings.get(name) ?? [];
|
|
6250
|
+
existing.push({ declaration, value });
|
|
6251
|
+
bindings.set(name, existing);
|
|
6252
|
+
}
|
|
6253
|
+
function nearestScopeContainer(node, context) {
|
|
6254
|
+
const owner = context.nearestFunction(node);
|
|
6255
|
+
let current = node.parent;
|
|
6256
|
+
while (current) {
|
|
6257
|
+
if (current.type === "block")
|
|
6258
|
+
return current;
|
|
6259
|
+
if (owner && current.id === owner.node.id)
|
|
6260
|
+
return owner.body ?? owner.node;
|
|
6261
|
+
current = current.parent;
|
|
6262
|
+
}
|
|
6263
|
+
return context.root;
|
|
6264
|
+
}
|
|
6265
|
+
function declarationVisibleAt(declaration, node, context) {
|
|
6266
|
+
if (declaration.startIndex >= node.startIndex)
|
|
6267
|
+
return false;
|
|
6268
|
+
const container = nearestScopeContainer(declaration, context);
|
|
6269
|
+
return node.startIndex >= container.startIndex && node.endIndex <= container.endIndex;
|
|
6270
|
+
}
|
|
6271
|
+
function functionParameterNames(node) {
|
|
6272
|
+
const result = new Set;
|
|
6273
|
+
const parameters = node.childForFieldName("parameters") ?? node.namedChildren.find((child) => child.type === "parameters");
|
|
6274
|
+
for (const parameter of parameters?.namedChildren ?? []) {
|
|
6275
|
+
if (parameter.type === "identifier")
|
|
6276
|
+
result.add(parameter.text);
|
|
6277
|
+
for (const child of parameter.namedChildren) {
|
|
6278
|
+
if (child.type === "identifier")
|
|
6279
|
+
result.add(child.text);
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
return result;
|
|
6283
|
+
}
|
|
6284
|
+
function loopBindsName2(node, name) {
|
|
6285
|
+
if (node.type !== "for_statement")
|
|
6286
|
+
return false;
|
|
6287
|
+
const header = node.text.split(/\bdo\b/s, 1)[0] ?? "";
|
|
6288
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6289
|
+
return new RegExp(`^\\s*for\\s+(?:${escaped}\\s*=|[^\\n]*\\b${escaped}\\b[^\\n]*\\bin\\b)`, "s").test(header);
|
|
6290
|
+
}
|
|
6291
|
+
function bindingShadowedAfterDeclaration(declaration, node, name) {
|
|
6292
|
+
let current = node.parent;
|
|
6293
|
+
while (current) {
|
|
6294
|
+
if (declaration.startIndex >= current.startIndex && declaration.endIndex <= current.endIndex)
|
|
6295
|
+
return false;
|
|
6296
|
+
if ((current.type === "function_definition" || current.type === "function_declaration") && functionParameterNames(current).has(name))
|
|
6297
|
+
return true;
|
|
6298
|
+
if (loopBindsName2(current, name))
|
|
6299
|
+
return true;
|
|
6300
|
+
current = current.parent;
|
|
6301
|
+
}
|
|
6302
|
+
return false;
|
|
6303
|
+
}
|
|
6304
|
+
function resolveSourceBinding(bindings, name, node, context) {
|
|
6305
|
+
const candidates = bindings.get(name);
|
|
6306
|
+
if (!candidates)
|
|
6307
|
+
return null;
|
|
6308
|
+
for (let index = candidates.length - 1;index >= 0; index -= 1) {
|
|
6309
|
+
const candidate = candidates[index];
|
|
6310
|
+
if (!declarationVisibleAt(candidate.declaration, node, context))
|
|
6311
|
+
continue;
|
|
6312
|
+
if (bindingShadowedAfterDeclaration(candidate.declaration, node, name))
|
|
6313
|
+
return null;
|
|
6314
|
+
return candidate.value;
|
|
6315
|
+
}
|
|
6316
|
+
return null;
|
|
6317
|
+
}
|
|
5707
6318
|
function sourceEffectImports(context) {
|
|
5708
6319
|
const result = new Map;
|
|
5709
6320
|
for (const node of context.walk(context.root)) {
|
|
5710
|
-
if (node.type !== "variable_declaration")
|
|
6321
|
+
if (node.type !== "variable_declaration" && node.type !== "assignment_statement")
|
|
5711
6322
|
continue;
|
|
5712
|
-
|
|
5713
|
-
if (!match)
|
|
6323
|
+
if (node.type === "assignment_statement" && node.parent?.type === "variable_declaration")
|
|
5714
6324
|
continue;
|
|
5715
|
-
const
|
|
5716
|
-
|
|
5717
|
-
|
|
6325
|
+
const { names, expressions } = bindingParts(node);
|
|
6326
|
+
for (let index = 0;index < names.length; index += 1) {
|
|
6327
|
+
const expression = expressions[index] ?? expressions[0];
|
|
6328
|
+
const requireMatch = expression?.type === "function_call" ? expression.text.match(/^\s*require\s*\((.*?)\)\s*$/s) : null;
|
|
6329
|
+
const summary = requireMatch ? resolveModuleReference(normalizeRequireTarget(requireMatch[1]), context.project.sourceEffects) : null;
|
|
6330
|
+
addSourceBinding(result, names[index], node, summary);
|
|
6331
|
+
}
|
|
5718
6332
|
}
|
|
5719
6333
|
return result;
|
|
5720
6334
|
}
|
|
@@ -5723,7 +6337,7 @@ function factorySummaryFromCall(call, context, imports) {
|
|
|
5723
6337
|
const match = path7.match(/^([A-Za-z_][A-Za-z0-9_]*)[.:]([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
5724
6338
|
if (!match)
|
|
5725
6339
|
return null;
|
|
5726
|
-
const summary = imports
|
|
6340
|
+
const summary = resolveSourceBinding(imports, match[1], call, context);
|
|
5727
6341
|
if (!summary?.instanceFactories.has(match[2]))
|
|
5728
6342
|
return null;
|
|
5729
6343
|
return summary;
|
|
@@ -5744,61 +6358,57 @@ function returnedFactorySummary(callback, context, imports) {
|
|
|
5744
6358
|
function sourceEffectInstances(context, imports) {
|
|
5745
6359
|
const result = new Map;
|
|
5746
6360
|
for (const node of context.walk(context.root)) {
|
|
5747
|
-
if (node.type !== "variable_declaration")
|
|
6361
|
+
if (node.type !== "variable_declaration" && node.type !== "assignment_statement")
|
|
5748
6362
|
continue;
|
|
5749
|
-
|
|
6363
|
+
if (node.type === "assignment_statement" && node.parent?.type === "variable_declaration")
|
|
6364
|
+
continue;
|
|
6365
|
+
const { names, expressions } = bindingParts(node);
|
|
5750
6366
|
for (let index = 0;index < names.length; index += 1) {
|
|
5751
6367
|
const expression = expressions[index] ?? expressions[0];
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
6368
|
+
let instance = null;
|
|
6369
|
+
if (expression?.type === "function_call") {
|
|
6370
|
+
const directFactory = factorySummaryFromCall(expression, context, imports);
|
|
6371
|
+
if (directFactory) {
|
|
6372
|
+
instance = {
|
|
6373
|
+
summary: directFactory,
|
|
6374
|
+
persistent: context.nearestFunction(node) === null
|
|
6375
|
+
};
|
|
6376
|
+
} else if (context.resolveCallPath(context.getCallPath(expression) ?? "") === "React.useMemo") {
|
|
5758
6377
|
const callback = context.callArguments(expression)[0];
|
|
5759
|
-
if (callback?.type === "function_definition")
|
|
5760
|
-
|
|
6378
|
+
if (callback?.type === "function_definition") {
|
|
6379
|
+
const memoizedFactory = returnedFactorySummary(callback, context, imports);
|
|
6380
|
+
if (memoizedFactory)
|
|
6381
|
+
instance = { summary: memoizedFactory, persistent: true };
|
|
6382
|
+
}
|
|
5761
6383
|
}
|
|
5762
|
-
} else if (expression
|
|
5763
|
-
|
|
5764
|
-
}
|
|
5765
|
-
if (summary)
|
|
5766
|
-
result.set(names[index], summary);
|
|
5767
|
-
}
|
|
5768
|
-
}
|
|
5769
|
-
let changed = true;
|
|
5770
|
-
while (changed) {
|
|
5771
|
-
changed = false;
|
|
5772
|
-
for (const node of context.walk(context.root)) {
|
|
5773
|
-
if (node.type !== "variable_declaration")
|
|
5774
|
-
continue;
|
|
5775
|
-
const { names, expressions } = declarationParts(node);
|
|
5776
|
-
for (let index = 0;index < names.length; index += 1) {
|
|
5777
|
-
if (result.has(names[index]))
|
|
5778
|
-
continue;
|
|
5779
|
-
const expression = expressions[index] ?? expressions[0];
|
|
5780
|
-
if (expression?.type !== "identifier")
|
|
5781
|
-
continue;
|
|
5782
|
-
const summary = result.get(expression.text);
|
|
5783
|
-
if (!summary)
|
|
5784
|
-
continue;
|
|
5785
|
-
result.set(names[index], summary);
|
|
5786
|
-
changed = true;
|
|
6384
|
+
} else if (expression?.type === "identifier") {
|
|
6385
|
+
instance = resolveSourceBinding(result, expression.text, expression, context);
|
|
5787
6386
|
}
|
|
6387
|
+
addSourceBinding(result, names[index], node, instance);
|
|
5788
6388
|
}
|
|
5789
6389
|
}
|
|
5790
6390
|
return result;
|
|
5791
6391
|
}
|
|
5792
6392
|
function sourceInferredEffectCall(call, context, imports, instances) {
|
|
5793
6393
|
const path7 = context.getCallPath(call)?.replace(/\s+/g, "") ?? "";
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
6394
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(path7)) {
|
|
6395
|
+
const direct = resolveSourceBinding(imports, path7, call, context);
|
|
6396
|
+
if (direct?.effectfulExport)
|
|
6397
|
+
return true;
|
|
6398
|
+
}
|
|
5797
6399
|
const member = path7.match(/^([A-Za-z_][A-Za-z0-9_]*)[.:]([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
5798
6400
|
if (!member)
|
|
5799
6401
|
return false;
|
|
5800
|
-
const
|
|
5801
|
-
|
|
6402
|
+
const instance = resolveSourceBinding(instances, member[1], call, context);
|
|
6403
|
+
if (instance) {
|
|
6404
|
+
if (instance.summary.effectfulMembers.has(member[2]))
|
|
6405
|
+
return true;
|
|
6406
|
+
return instance.persistent && instance.summary.mutatingMembers.has(member[2]);
|
|
6407
|
+
}
|
|
6408
|
+
const imported = resolveSourceBinding(imports, member[1], call, context);
|
|
6409
|
+
if (!imported)
|
|
6410
|
+
return false;
|
|
6411
|
+
return imported.effectfulMembers.has(member[2]) || imported.mutatingMembers.has(member[2]);
|
|
5802
6412
|
}
|
|
5803
6413
|
var RENDER_EFFECT_PATTERNS = [
|
|
5804
6414
|
/^Instance\.new$/,
|
|
@@ -5810,7 +6420,6 @@ var RENDER_EFFECT_PATTERNS = [
|
|
|
5810
6420
|
/BindToRenderStep$/,
|
|
5811
6421
|
/BindToSimulation$/,
|
|
5812
6422
|
/:Destroy$/,
|
|
5813
|
-
/:render$/,
|
|
5814
6423
|
/:unmount$/
|
|
5815
6424
|
];
|
|
5816
6425
|
function hasNestedKnownRenderSideEffect(call, context) {
|
|
@@ -5822,7 +6431,7 @@ function hasNestedKnownRenderSideEffect(call, context) {
|
|
|
5822
6431
|
const path7 = context.resolveCallPath(context.getCallPath(node) ?? "");
|
|
5823
6432
|
if (path7 === "task.spawn" || path7 === "task.defer" || path7 === "task.delay")
|
|
5824
6433
|
return true;
|
|
5825
|
-
if (knownYieldReason(path7) || RENDER_EFFECT_PATTERNS.some((pattern) => pattern.test(path7)))
|
|
6434
|
+
if (knownYieldReason(path7, node, context) || RENDER_EFFECT_PATTERNS.some((pattern) => pattern.test(path7)))
|
|
5826
6435
|
return true;
|
|
5827
6436
|
}
|
|
5828
6437
|
return false;
|
|
@@ -5839,7 +6448,7 @@ var noYieldInRender = {
|
|
|
5839
6448
|
if (!component || !context.isDirectlyExecutedInFunction(call, component))
|
|
5840
6449
|
continue;
|
|
5841
6450
|
const path7 = context.resolveCallPath(context.getCallPath(call) ?? "");
|
|
5842
|
-
const reason = knownYieldReason(path7);
|
|
6451
|
+
const reason = knownYieldReason(path7, call, context);
|
|
5843
6452
|
if (!reason)
|
|
5844
6453
|
continue;
|
|
5845
6454
|
diagnostics.push({
|
|
@@ -6472,7 +7081,7 @@ function directlyReadsValue(context, owner, binding) {
|
|
|
6472
7081
|
continue;
|
|
6473
7082
|
if (isNestedFunctionFromOwner(context, node, owner))
|
|
6474
7083
|
continue;
|
|
6475
|
-
if (
|
|
7084
|
+
if (isBindingShadowedBetween(node, owner, binding.valueName, binding.declaration))
|
|
6476
7085
|
continue;
|
|
6477
7086
|
return true;
|
|
6478
7087
|
}
|
|
@@ -6480,7 +7089,7 @@ function directlyReadsValue(context, owner, binding) {
|
|
|
6480
7089
|
if (!fn.body)
|
|
6481
7090
|
continue;
|
|
6482
7091
|
for (const node of context.walk(fn.body)) {
|
|
6483
|
-
if (isIdentifierRead(node, binding.valueName) && !
|
|
7092
|
+
if (isIdentifierRead(node, binding.valueName) && !isBindingShadowedBetween(node, owner, binding.valueName, binding.declaration))
|
|
6484
7093
|
return true;
|
|
6485
7094
|
}
|
|
6486
7095
|
}
|
|
@@ -6660,13 +7269,20 @@ var noDirectStateMutation = {
|
|
|
6660
7269
|
let mutates = false;
|
|
6661
7270
|
if (node.type === "assignment_statement" || node.type === "update_statement") {
|
|
6662
7271
|
mutates = new RegExp(`^\\s*${escaped}\\s*(?:\\.|\\[)`).test(node.text);
|
|
6663
|
-
} else if (node.type === "function_call") {
|
|
6664
|
-
const
|
|
6665
|
-
|
|
6666
|
-
|
|
7272
|
+
} else if (node.type === "function_call" && mightMutateParameters(context, node)) {
|
|
7273
|
+
const arguments_ = context.callArguments(node);
|
|
7274
|
+
for (const index of mutatedParameterIndexesForCall(context, node, owner)) {
|
|
7275
|
+
const argument = arguments_[index];
|
|
7276
|
+
if (!argument)
|
|
7277
|
+
continue;
|
|
7278
|
+
const origin = mutationOriginForExpression(context, argument, owner);
|
|
7279
|
+
if (origin.kind === "state" && origin.binding === binding) {
|
|
7280
|
+
mutates = true;
|
|
7281
|
+
break;
|
|
7282
|
+
}
|
|
6667
7283
|
}
|
|
6668
7284
|
}
|
|
6669
|
-
if (!mutates ||
|
|
7285
|
+
if (!mutates || isBindingShadowedBetween(node, owner, binding.valueName, binding.declaration))
|
|
6670
7286
|
continue;
|
|
6671
7287
|
const key = `${node.startIndex}:${binding.valueName}`;
|
|
6672
7288
|
if (seen.has(key))
|
|
@@ -6993,7 +7609,6 @@ class AnalysisWorkerPool {
|
|
|
6993
7609
|
// src/project-model.ts
|
|
6994
7610
|
import fs6 from "fs";
|
|
6995
7611
|
import path7 from "path";
|
|
6996
|
-
var HIGH_FREQUENCY_SOURCE = /(?:RenderStepped|Heartbeat|Stepped|PreRender|PreSimulation|PostSimulation)\s*:\s*Connect|BindToRenderStep|BindToSimulation/;
|
|
6997
7612
|
var EXTERNAL_UPDATE_SOURCE = /(?::|\.)(?:Connect|Once|Subscribe|Observe|Listen|Watch)\s*\(|\b(?:subscribe|observe|listen|watch)[A-Za-z0-9_]*\s*\(|GetPropertyChangedSignal\s*\(|\.Changed\b/i;
|
|
6998
7613
|
var CALLBACK_PARAMETER_NAME = /^(?:callback|handler|listener|subscriber|observer|effect|fn)$/i;
|
|
6999
7614
|
function exportedFunctionName(source) {
|
|
@@ -7330,7 +7945,7 @@ function findExternalCallbackFunction(source) {
|
|
|
7330
7945
|
return {
|
|
7331
7946
|
name,
|
|
7332
7947
|
callbackParameterIndexes,
|
|
7333
|
-
highFrequency:
|
|
7948
|
+
highFrequency: sourceHasHighFrequencyRunService(source)
|
|
7334
7949
|
};
|
|
7335
7950
|
}
|
|
7336
7951
|
function findBindingCandidateHook(source) {
|
|
@@ -7354,7 +7969,7 @@ function findBindingCandidateHook(source) {
|
|
|
7354
7969
|
const setterPassedToSubscription = new RegExp(`(?::|\\.)(?:Connect|Once|Subscribe|Observe|Listen|Watch)\\s*\\(\\s*${escapedSetter}\\b`, "i").test(source);
|
|
7355
7970
|
if (!setterCalled && !setterPassedToSubscription)
|
|
7356
7971
|
return null;
|
|
7357
|
-
const highFrequency =
|
|
7972
|
+
const highFrequency = sourceHasHighFrequencyRunService(source);
|
|
7358
7973
|
const external = highFrequency || EXTERNAL_UPDATE_SOURCE.test(source);
|
|
7359
7974
|
if (!external)
|
|
7360
7975
|
return null;
|
|
@@ -7901,6 +8516,27 @@ function topLevelReturnName(root) {
|
|
|
7901
8516
|
}
|
|
7902
8517
|
return null;
|
|
7903
8518
|
}
|
|
8519
|
+
function topLevelImports2(root, moduleAliases) {
|
|
8520
|
+
const imports = new Map;
|
|
8521
|
+
for (const node of root.namedChildren) {
|
|
8522
|
+
if (node.type !== "variable_declaration")
|
|
8523
|
+
continue;
|
|
8524
|
+
const { names, expressions } = declarationParts3(node);
|
|
8525
|
+
for (let index = 0;index < names.length; index += 1) {
|
|
8526
|
+
const expression = expressions[index] ?? expressions[0];
|
|
8527
|
+
if (expression?.type !== "function_call")
|
|
8528
|
+
continue;
|
|
8529
|
+
const text = expression.text.trim();
|
|
8530
|
+
const match = text.match(/^require\s*\((.*?)\)\s*$/s);
|
|
8531
|
+
if (!match)
|
|
8532
|
+
continue;
|
|
8533
|
+
const moduleId = resolveModuleReference(normalizeRequireTarget(match[1]), moduleAliases);
|
|
8534
|
+
if (moduleId)
|
|
8535
|
+
imports.set(names[index], moduleId);
|
|
8536
|
+
}
|
|
8537
|
+
}
|
|
8538
|
+
return imports;
|
|
8539
|
+
}
|
|
7904
8540
|
function parameterNames3(node) {
|
|
7905
8541
|
const parameters = child(node, "parameters");
|
|
7906
8542
|
if (!parameters)
|
|
@@ -7915,7 +8551,7 @@ function parameterNames3(node) {
|
|
|
7915
8551
|
}
|
|
7916
8552
|
return result;
|
|
7917
8553
|
}
|
|
7918
|
-
function
|
|
8554
|
+
function declarationParts3(node) {
|
|
7919
8555
|
if (node.type !== "variable_declaration")
|
|
7920
8556
|
return { names: [], expressions: [] };
|
|
7921
8557
|
const assignment = child(node, "assignment_statement");
|
|
@@ -7951,6 +8587,45 @@ function rootIdentifier2(node) {
|
|
|
7951
8587
|
const text = node.text.trim();
|
|
7952
8588
|
return text.match(/^([A-Za-z_][A-Za-z0-9_]*)/)?.[1] ?? null;
|
|
7953
8589
|
}
|
|
8590
|
+
function callArguments(node) {
|
|
8591
|
+
if (node.type !== "function_call")
|
|
8592
|
+
return [];
|
|
8593
|
+
return node.childForFieldName("arguments")?.namedChildren ?? [];
|
|
8594
|
+
}
|
|
8595
|
+
function builtinMutatedArgumentIndexes(path9, argumentCount) {
|
|
8596
|
+
switch (path9.replace(/\s+/g, "")) {
|
|
8597
|
+
case "rawset":
|
|
8598
|
+
case "setmetatable":
|
|
8599
|
+
case "table.clear":
|
|
8600
|
+
case "table.freeze":
|
|
8601
|
+
case "table.insert":
|
|
8602
|
+
case "table.remove":
|
|
8603
|
+
case "table.sort":
|
|
8604
|
+
return argumentCount > 0 ? [0] : [];
|
|
8605
|
+
case "table.move":
|
|
8606
|
+
return argumentCount >= 5 ? [4] : argumentCount > 0 ? [0] : [];
|
|
8607
|
+
default:
|
|
8608
|
+
return [];
|
|
8609
|
+
}
|
|
8610
|
+
}
|
|
8611
|
+
function expressionCreatesOwnedValue(node, owned) {
|
|
8612
|
+
if (node.type === "table_constructor" || node.type === "function_definition")
|
|
8613
|
+
return true;
|
|
8614
|
+
if (node.type !== "function_call")
|
|
8615
|
+
return false;
|
|
8616
|
+
const path9 = callPath(node)?.replace(/\s+/g, "") ?? "";
|
|
8617
|
+
if (path9 === "table.clone" || path9 === "table.create" || path9 === "table.pack")
|
|
8618
|
+
return true;
|
|
8619
|
+
if (path9 !== "setmetatable" && path9 !== "table.freeze")
|
|
8620
|
+
return false;
|
|
8621
|
+
const first = callArguments(node)[0];
|
|
8622
|
+
if (!first)
|
|
8623
|
+
return false;
|
|
8624
|
+
if (first.type === "table_constructor")
|
|
8625
|
+
return true;
|
|
8626
|
+
const root = rootIdentifier2(first);
|
|
8627
|
+
return Boolean(root && owned.has(root));
|
|
8628
|
+
}
|
|
7954
8629
|
function directNodes(body) {
|
|
7955
8630
|
const result = [];
|
|
7956
8631
|
const visit = (node) => {
|
|
@@ -7981,7 +8656,14 @@ function topLevelFunctionRecords(record) {
|
|
|
7981
8656
|
body,
|
|
7982
8657
|
parameters: parameterNames3(node),
|
|
7983
8658
|
directEffect: false,
|
|
7984
|
-
|
|
8659
|
+
mutatesReceiver: false,
|
|
8660
|
+
mutatedParameterIndexes: new Set,
|
|
8661
|
+
dependencies: new Set,
|
|
8662
|
+
mutationCalls: [],
|
|
8663
|
+
parameterOrigins: new Map,
|
|
8664
|
+
externalRoots: new Set,
|
|
8665
|
+
ownedRoots: new Set,
|
|
8666
|
+
localNames: new Set
|
|
7985
8667
|
});
|
|
7986
8668
|
};
|
|
7987
8669
|
for (const node of record.tree.rootNode.namedChildren) {
|
|
@@ -7998,7 +8680,7 @@ function topLevelFunctionRecords(record) {
|
|
|
7998
8680
|
continue;
|
|
7999
8681
|
}
|
|
8000
8682
|
if (node.type === "variable_declaration") {
|
|
8001
|
-
const { names, expressions } =
|
|
8683
|
+
const { names, expressions } = declarationParts3(node);
|
|
8002
8684
|
for (let index = 0;index < names.length; index += 1) {
|
|
8003
8685
|
const expression = expressions[index] ?? expressions[0];
|
|
8004
8686
|
if (expression?.type !== "function_definition")
|
|
@@ -8050,7 +8732,7 @@ function moduleLevelInstanceAliases(record, summariesByModuleId) {
|
|
|
8050
8732
|
for (const node of record.tree.rootNode.namedChildren) {
|
|
8051
8733
|
if (node.type !== "variable_declaration")
|
|
8052
8734
|
continue;
|
|
8053
|
-
const { names, expressions } =
|
|
8735
|
+
const { names, expressions } = declarationParts3(node);
|
|
8054
8736
|
for (let index = 0;index < names.length; index += 1) {
|
|
8055
8737
|
const expression = expressions[index] ?? expressions[0];
|
|
8056
8738
|
if (expression?.type !== "function_call")
|
|
@@ -8069,34 +8751,50 @@ function moduleLevelInstanceAliases(record, summariesByModuleId) {
|
|
|
8069
8751
|
}
|
|
8070
8752
|
function analyzeFunction(fn, record, localFunctions, memberFunctions, moduleSummaries, moduleInstances, exportedFunctions) {
|
|
8071
8753
|
const parameters = new Set(fn.parameters);
|
|
8072
|
-
|
|
8754
|
+
const parameterOrigins = new Map(fn.parameters.map((name, index) => [name, index]));
|
|
8755
|
+
if (fn.method) {
|
|
8073
8756
|
parameters.add("self");
|
|
8757
|
+
parameterOrigins.set("self", -1);
|
|
8758
|
+
}
|
|
8074
8759
|
const locals = new Set;
|
|
8075
8760
|
const owned = new Set;
|
|
8076
8761
|
const externalAliases = new Set;
|
|
8762
|
+
const refLocals = new Set;
|
|
8077
8763
|
const instanceAliases = new Map(moduleInstances);
|
|
8078
8764
|
const nodes = directNodes(fn.body);
|
|
8079
8765
|
for (const node of nodes) {
|
|
8766
|
+
if (node.type === "function_declaration" && node.id !== fn.node.id) {
|
|
8767
|
+
const declared = node.childForFieldName("name")?.text.replace(/\s+/g, "") ?? "";
|
|
8768
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(declared))
|
|
8769
|
+
locals.add(declared);
|
|
8770
|
+
continue;
|
|
8771
|
+
}
|
|
8080
8772
|
if (node.type !== "variable_declaration")
|
|
8081
8773
|
continue;
|
|
8082
|
-
const { names, expressions } =
|
|
8774
|
+
const { names, expressions } = declarationParts3(node);
|
|
8083
8775
|
for (let index = 0;index < names.length; index += 1) {
|
|
8084
8776
|
const name = names[index];
|
|
8085
8777
|
const expression = expressions[index] ?? expressions[0];
|
|
8086
8778
|
locals.add(name);
|
|
8087
8779
|
if (!expression)
|
|
8088
8780
|
continue;
|
|
8089
|
-
|
|
8781
|
+
const createsOwnedValue = expressionCreatesOwnedValue(expression, owned);
|
|
8782
|
+
if (createsOwnedValue)
|
|
8090
8783
|
owned.add(name);
|
|
8091
8784
|
const root = rootIdentifier2(expression);
|
|
8092
|
-
|
|
8785
|
+
const parameterOrigin = root ? parameterOrigins.get(root) : undefined;
|
|
8786
|
+
if (!createsOwnedValue && root && parameterOrigin !== undefined) {
|
|
8787
|
+
parameterOrigins.set(name, parameterOrigin);
|
|
8788
|
+
} else if (!createsOwnedValue && root && (externalAliases.has(root) || !locals.has(root) && root !== name)) {
|
|
8093
8789
|
externalAliases.add(name);
|
|
8094
8790
|
} else if (root && owned.has(root)) {
|
|
8095
8791
|
owned.add(name);
|
|
8096
8792
|
}
|
|
8097
8793
|
if (expression.type === "function_call") {
|
|
8098
|
-
const path9 = callPath(expression);
|
|
8099
|
-
|
|
8794
|
+
const path9 = callPath(expression)?.replace(/\s+/g, "") ?? "";
|
|
8795
|
+
if (path9 === "React.useRef")
|
|
8796
|
+
refLocals.add(name);
|
|
8797
|
+
const match = path9.match(/^([A-Za-z_][A-Za-z0-9_]*)[.:]([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
8100
8798
|
if (match) {
|
|
8101
8799
|
const moduleId = record.imports.get(match[1]);
|
|
8102
8800
|
const summary = moduleId ? moduleSummaries.get(moduleId) : null;
|
|
@@ -8106,6 +8804,10 @@ function analyzeFunction(fn, record, localFunctions, memberFunctions, moduleSumm
|
|
|
8106
8804
|
}
|
|
8107
8805
|
}
|
|
8108
8806
|
}
|
|
8807
|
+
fn.parameterOrigins = parameterOrigins;
|
|
8808
|
+
fn.externalRoots = externalAliases;
|
|
8809
|
+
fn.ownedRoots = owned;
|
|
8810
|
+
fn.localNames = locals;
|
|
8109
8811
|
for (const node of nodes) {
|
|
8110
8812
|
if (node.type === "assignment_statement" && node.parent?.type !== "variable_declaration") {
|
|
8111
8813
|
const { left } = assignmentSides(node);
|
|
@@ -8120,7 +8822,17 @@ function analyzeFunction(fn, record, localFunctions, memberFunctions, moduleSumm
|
|
|
8120
8822
|
continue;
|
|
8121
8823
|
if (owned.has(root))
|
|
8122
8824
|
continue;
|
|
8123
|
-
if (
|
|
8825
|
+
if (refLocals.has(root) && target.text.replace(/\s+/g, "") === `${root}.current`)
|
|
8826
|
+
continue;
|
|
8827
|
+
const parameterOrigin = parameterOrigins.get(root);
|
|
8828
|
+
if (parameterOrigin !== undefined) {
|
|
8829
|
+
if (parameterOrigin === -1)
|
|
8830
|
+
fn.mutatesReceiver = true;
|
|
8831
|
+
else
|
|
8832
|
+
fn.mutatedParameterIndexes.add(parameterOrigin);
|
|
8833
|
+
continue;
|
|
8834
|
+
}
|
|
8835
|
+
if (externalAliases.has(root) || !locals.has(root))
|
|
8124
8836
|
fn.directEffect = true;
|
|
8125
8837
|
}
|
|
8126
8838
|
continue;
|
|
@@ -8130,39 +8842,174 @@ function analyzeFunction(fn, record, localFunctions, memberFunctions, moduleSumm
|
|
|
8130
8842
|
const path9 = callPath(node);
|
|
8131
8843
|
if (!path9)
|
|
8132
8844
|
continue;
|
|
8133
|
-
const
|
|
8134
|
-
|
|
8845
|
+
const arguments_ = callArguments(node);
|
|
8846
|
+
for (const index of builtinMutatedArgumentIndexes(path9, arguments_.length)) {
|
|
8847
|
+
markMutationThroughRoot(fn, rootIdentifier2(arguments_[index]));
|
|
8848
|
+
}
|
|
8849
|
+
const localTarget = !locals.has(path9) && !parameters.has(path9) ? localFunctions.get(path9) : null;
|
|
8850
|
+
if (localTarget) {
|
|
8135
8851
|
fn.dependencies.add(localTarget);
|
|
8136
|
-
|
|
8852
|
+
fn.mutationCalls.push({
|
|
8853
|
+
targetId: localTarget,
|
|
8854
|
+
receiverRoot: null,
|
|
8855
|
+
argumentRoots: arguments_.map((argument) => rootIdentifier2(argument))
|
|
8856
|
+
});
|
|
8857
|
+
}
|
|
8858
|
+
const sameMember = path9.match(/^([A-Za-z_][A-Za-z0-9_]*)([:.])([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
8137
8859
|
if (sameMember) {
|
|
8138
|
-
const receiver =
|
|
8139
|
-
if (receiver === "self" || receiver === record.exportName) {
|
|
8140
|
-
const target = memberFunctions.get(sameMember[
|
|
8141
|
-
if (target)
|
|
8860
|
+
const receiver = sameMember[1];
|
|
8861
|
+
if (receiver === "self" || receiver === record.exportName && !locals.has(receiver) && !parameters.has(receiver)) {
|
|
8862
|
+
const target = memberFunctions.get(sameMember[3]);
|
|
8863
|
+
if (target) {
|
|
8142
8864
|
fn.dependencies.add(target);
|
|
8865
|
+
fn.mutationCalls.push({
|
|
8866
|
+
targetId: target,
|
|
8867
|
+
receiverRoot: sameMember[2] === ":" ? receiver : null,
|
|
8868
|
+
argumentRoots: arguments_.map((argument) => rootIdentifier2(argument))
|
|
8869
|
+
});
|
|
8870
|
+
}
|
|
8143
8871
|
}
|
|
8144
8872
|
}
|
|
8145
|
-
const importedMember = path9.match(/^([A-Za-z_][A-Za-z0-9_]*)[.:]([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
8873
|
+
const importedMember = path9.match(/^([A-Za-z_][A-Za-z0-9_]*)([.:])([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
8146
8874
|
if (importedMember) {
|
|
8147
|
-
const
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8875
|
+
const root = importedMember[1];
|
|
8876
|
+
const moduleId = instanceAliases.get(root) ?? (!locals.has(root) && !parameters.has(root) ? record.imports.get(root) : undefined);
|
|
8877
|
+
if (moduleId) {
|
|
8878
|
+
const target = `${moduleId}::member:${importedMember[3]}`;
|
|
8879
|
+
fn.dependencies.add(target);
|
|
8880
|
+
fn.mutationCalls.push({
|
|
8881
|
+
targetId: target,
|
|
8882
|
+
receiverRoot: importedMember[2] === ":" ? root : null,
|
|
8883
|
+
argumentRoots: arguments_.map((argument) => rootIdentifier2(argument))
|
|
8884
|
+
});
|
|
8885
|
+
}
|
|
8886
|
+
} else if (!locals.has(path9) && !parameters.has(path9)) {
|
|
8151
8887
|
const moduleId = record.imports.get(path9);
|
|
8152
8888
|
const target = moduleId ? exportedFunctions.get(moduleId) : null;
|
|
8153
|
-
if (target)
|
|
8889
|
+
if (target) {
|
|
8154
8890
|
fn.dependencies.add(target);
|
|
8891
|
+
fn.mutationCalls.push({
|
|
8892
|
+
targetId: target,
|
|
8893
|
+
receiverRoot: null,
|
|
8894
|
+
argumentRoots: arguments_.map((argument) => rootIdentifier2(argument))
|
|
8895
|
+
});
|
|
8896
|
+
}
|
|
8897
|
+
}
|
|
8898
|
+
}
|
|
8899
|
+
}
|
|
8900
|
+
function markMutationThroughRoot(fn, root) {
|
|
8901
|
+
if (!root)
|
|
8902
|
+
return false;
|
|
8903
|
+
const parameterOrigin = fn.parameterOrigins.get(root);
|
|
8904
|
+
if (parameterOrigin !== undefined) {
|
|
8905
|
+
if (parameterOrigin === -1) {
|
|
8906
|
+
if (fn.mutatesReceiver)
|
|
8907
|
+
return false;
|
|
8908
|
+
fn.mutatesReceiver = true;
|
|
8909
|
+
return true;
|
|
8910
|
+
}
|
|
8911
|
+
if (fn.mutatedParameterIndexes.has(parameterOrigin))
|
|
8912
|
+
return false;
|
|
8913
|
+
fn.mutatedParameterIndexes.add(parameterOrigin);
|
|
8914
|
+
return true;
|
|
8915
|
+
}
|
|
8916
|
+
if (fn.ownedRoots.has(root))
|
|
8917
|
+
return false;
|
|
8918
|
+
if (fn.externalRoots.has(root) || !fn.localNames.has(root)) {
|
|
8919
|
+
if (fn.directEffect)
|
|
8920
|
+
return false;
|
|
8921
|
+
fn.directEffect = true;
|
|
8922
|
+
return true;
|
|
8923
|
+
}
|
|
8924
|
+
if (fn.localNames.has(root))
|
|
8925
|
+
return false;
|
|
8926
|
+
return false;
|
|
8927
|
+
}
|
|
8928
|
+
function propagateLocalMutationEffects(functions) {
|
|
8929
|
+
const byId = new Map(functions.map((fn) => [fn.id, fn]));
|
|
8930
|
+
let changed = true;
|
|
8931
|
+
while (changed) {
|
|
8932
|
+
changed = false;
|
|
8933
|
+
for (const fn of functions) {
|
|
8934
|
+
for (const call of fn.mutationCalls) {
|
|
8935
|
+
const target = byId.get(call.targetId);
|
|
8936
|
+
if (!target)
|
|
8937
|
+
continue;
|
|
8938
|
+
if (target.mutatesReceiver && markMutationThroughRoot(fn, call.receiverRoot))
|
|
8939
|
+
changed = true;
|
|
8940
|
+
for (const index of target.mutatedParameterIndexes) {
|
|
8941
|
+
if (markMutationThroughRoot(fn, call.argumentRoots[index] ?? null))
|
|
8942
|
+
changed = true;
|
|
8943
|
+
}
|
|
8944
|
+
}
|
|
8945
|
+
}
|
|
8946
|
+
}
|
|
8947
|
+
}
|
|
8948
|
+
function mutationOriginForRoot(fn, root) {
|
|
8949
|
+
if (!root)
|
|
8950
|
+
return null;
|
|
8951
|
+
const parameterOrigin = fn.parameterOrigins.get(root);
|
|
8952
|
+
if (parameterOrigin === -1)
|
|
8953
|
+
return { kind: "receiver" };
|
|
8954
|
+
if (parameterOrigin !== undefined)
|
|
8955
|
+
return { kind: "parameter", index: parameterOrigin };
|
|
8956
|
+
if (fn.ownedRoots.has(root) || fn.localNames.has(root))
|
|
8957
|
+
return { kind: "local" };
|
|
8958
|
+
if (fn.externalRoots.has(root) || !fn.localNames.has(root))
|
|
8959
|
+
return { kind: "external" };
|
|
8960
|
+
return null;
|
|
8961
|
+
}
|
|
8962
|
+
function cachedMutationCalls(fn) {
|
|
8963
|
+
return fn.mutationCalls.map((call) => ({
|
|
8964
|
+
targetId: call.targetId,
|
|
8965
|
+
receiverOrigin: mutationOriginForRoot(fn, call.receiverRoot),
|
|
8966
|
+
argumentOrigins: call.argumentRoots.map((root) => mutationOriginForRoot(fn, root))
|
|
8967
|
+
}));
|
|
8968
|
+
}
|
|
8969
|
+
function applyCachedMutationOrigin(fn, origin) {
|
|
8970
|
+
if (!origin || origin.kind === "local")
|
|
8971
|
+
return false;
|
|
8972
|
+
if (origin.kind === "external") {
|
|
8973
|
+
if (fn.directEffect)
|
|
8974
|
+
return false;
|
|
8975
|
+
fn.directEffect = true;
|
|
8976
|
+
return true;
|
|
8977
|
+
}
|
|
8978
|
+
if (origin.kind === "receiver") {
|
|
8979
|
+
if (fn.mutatesReceiver)
|
|
8980
|
+
return false;
|
|
8981
|
+
fn.mutatesReceiver = true;
|
|
8982
|
+
return true;
|
|
8983
|
+
}
|
|
8984
|
+
if (fn.mutatedParameterIndexes.includes(origin.index))
|
|
8985
|
+
return false;
|
|
8986
|
+
fn.mutatedParameterIndexes.push(origin.index);
|
|
8987
|
+
fn.mutatedParameterIndexes.sort((a, b) => a - b);
|
|
8988
|
+
return true;
|
|
8989
|
+
}
|
|
8990
|
+
function propagateMutationEffects(functions) {
|
|
8991
|
+
const byId = new Map(functions.map((fn) => [fn.id, fn]));
|
|
8992
|
+
let changed = true;
|
|
8993
|
+
while (changed) {
|
|
8994
|
+
changed = false;
|
|
8995
|
+
for (const fn of functions) {
|
|
8996
|
+
for (const call of fn.mutationCalls) {
|
|
8997
|
+
const target = byId.get(call.targetId);
|
|
8998
|
+
if (!target)
|
|
8999
|
+
continue;
|
|
9000
|
+
if (target.mutatesReceiver && applyCachedMutationOrigin(fn, call.receiverOrigin))
|
|
9001
|
+
changed = true;
|
|
9002
|
+
for (const index of target.mutatedParameterIndexes) {
|
|
9003
|
+
if (applyCachedMutationOrigin(fn, call.argumentOrigins[index] ?? null))
|
|
9004
|
+
changed = true;
|
|
9005
|
+
}
|
|
9006
|
+
}
|
|
8155
9007
|
}
|
|
8156
9008
|
}
|
|
8157
9009
|
}
|
|
8158
9010
|
async function indexEffectModuleForWorker(input, moduleAliases) {
|
|
8159
9011
|
const tree = await parseLuau(input.source);
|
|
8160
|
-
const imports =
|
|
8161
|
-
for (const match of input.source.matchAll(/\blocal\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*require\s*\((.*?)\)/gs)) {
|
|
8162
|
-
const moduleId = resolveModuleReference(normalizeRequireTarget(match[2]), moduleAliases);
|
|
8163
|
-
if (moduleId)
|
|
8164
|
-
imports.set(match[1], moduleId);
|
|
8165
|
-
}
|
|
9012
|
+
const imports = topLevelImports2(tree.rootNode, moduleAliases);
|
|
8166
9013
|
const record = {
|
|
8167
9014
|
id: input.id,
|
|
8168
9015
|
keys: input.keys,
|
|
@@ -8203,6 +9050,7 @@ function analyzeEffectModuleForWorker(state, moduleSummaries, exportedFunctions)
|
|
|
8203
9050
|
for (const fn of state.functions) {
|
|
8204
9051
|
analyzeFunction(fn, state.record, localFunctions, memberFunctions, moduleSummaries, moduleInstances, exportedFunctions);
|
|
8205
9052
|
}
|
|
9053
|
+
propagateLocalMutationEffects(state.functions);
|
|
8206
9054
|
return {
|
|
8207
9055
|
id: state.record.id,
|
|
8208
9056
|
functions: state.functions.map((fn) => ({
|
|
@@ -8212,7 +9060,10 @@ function analyzeEffectModuleForWorker(state, moduleSummaries, exportedFunctions)
|
|
|
8212
9060
|
memberName: fn.memberName,
|
|
8213
9061
|
exported: fn.exported,
|
|
8214
9062
|
directEffect: fn.directEffect,
|
|
8215
|
-
|
|
9063
|
+
mutatesReceiver: fn.mutatesReceiver,
|
|
9064
|
+
mutatedParameterIndexes: [...fn.mutatedParameterIndexes].sort((a, b) => a - b),
|
|
9065
|
+
dependencies: [...fn.dependencies].sort(),
|
|
9066
|
+
mutationCalls: cachedMutationCalls(fn)
|
|
8216
9067
|
}))
|
|
8217
9068
|
};
|
|
8218
9069
|
}
|
|
@@ -8289,6 +9140,10 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
8289
9140
|
summariesByModuleId.set(identity.id, {
|
|
8290
9141
|
effectfulMembers: new Set,
|
|
8291
9142
|
effectfulExport: false,
|
|
9143
|
+
mutatingMembers: new Set,
|
|
9144
|
+
mutatingExportParameters: new Set,
|
|
9145
|
+
mutatingMemberParameters: new Map,
|
|
9146
|
+
localMutatingParameters: new Map,
|
|
8292
9147
|
instanceFactories: new Set(indexed.instanceFactories)
|
|
8293
9148
|
});
|
|
8294
9149
|
if (indexed.exportedFunctionId)
|
|
@@ -8301,6 +9156,10 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
8301
9156
|
summariesByModuleId.set(identity.id, {
|
|
8302
9157
|
effectfulMembers: new Set,
|
|
8303
9158
|
effectfulExport: false,
|
|
9159
|
+
mutatingMembers: new Set,
|
|
9160
|
+
mutatingExportParameters: new Set,
|
|
9161
|
+
mutatingMemberParameters: new Map,
|
|
9162
|
+
localMutatingParameters: new Map,
|
|
8304
9163
|
instanceFactories: new Set(cached?.instanceFactories ?? [])
|
|
8305
9164
|
});
|
|
8306
9165
|
const exported = functions2.find((fn) => fn.exported && !fn.memberName);
|
|
@@ -8336,12 +9195,7 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
8336
9195
|
const source = candidate.source ?? fs7.readFileSync(candidate.absolutePath, "utf8");
|
|
8337
9196
|
const tree = await parseLuau(source);
|
|
8338
9197
|
parseCache?.set(relativePath, { source, tree });
|
|
8339
|
-
const imports =
|
|
8340
|
-
for (const match of source.matchAll(/\blocal\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*require\s*\((.*?)\)/gs)) {
|
|
8341
|
-
const moduleId = resolveModuleReference(normalizeRequireTarget(match[2]), moduleAliases);
|
|
8342
|
-
if (moduleId)
|
|
8343
|
-
imports.set(match[1], moduleId);
|
|
8344
|
-
}
|
|
9198
|
+
const imports = topLevelImports2(tree.rootNode, moduleAliases);
|
|
8345
9199
|
rawRecords.set(identity.id, {
|
|
8346
9200
|
id: identity.id,
|
|
8347
9201
|
keys: identity.keys,
|
|
@@ -8366,6 +9220,10 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
8366
9220
|
const summary = {
|
|
8367
9221
|
effectfulMembers: new Set,
|
|
8368
9222
|
effectfulExport: false,
|
|
9223
|
+
mutatingMembers: new Set,
|
|
9224
|
+
mutatingExportParameters: new Set,
|
|
9225
|
+
mutatingMemberParameters: new Map,
|
|
9226
|
+
localMutatingParameters: new Map,
|
|
8369
9227
|
instanceFactories: new Set
|
|
8370
9228
|
};
|
|
8371
9229
|
for (const fn of functions2) {
|
|
@@ -8380,6 +9238,10 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
8380
9238
|
summariesByModuleId.set(identity.id, {
|
|
8381
9239
|
effectfulMembers: new Set,
|
|
8382
9240
|
effectfulExport: false,
|
|
9241
|
+
mutatingMembers: new Set,
|
|
9242
|
+
mutatingExportParameters: new Set,
|
|
9243
|
+
mutatingMemberParameters: new Map,
|
|
9244
|
+
localMutatingParameters: new Map,
|
|
8383
9245
|
instanceFactories: new Set(cached?.instanceFactories ?? [])
|
|
8384
9246
|
});
|
|
8385
9247
|
}
|
|
@@ -8421,6 +9283,7 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
8421
9283
|
for (const fn of functions2) {
|
|
8422
9284
|
analyzeFunction(fn, record, localFunctions, memberFunctions, summariesByModuleId, moduleInstances, exportedFunctions);
|
|
8423
9285
|
}
|
|
9286
|
+
propagateLocalMutationEffects(functions2);
|
|
8424
9287
|
analyzedFunctionStatesByModule.set(moduleId, functions2.map((fn) => ({
|
|
8425
9288
|
id: fn.id,
|
|
8426
9289
|
moduleId: fn.moduleId,
|
|
@@ -8428,7 +9291,10 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
8428
9291
|
memberName: fn.memberName,
|
|
8429
9292
|
exported: fn.exported,
|
|
8430
9293
|
directEffect: fn.directEffect,
|
|
8431
|
-
|
|
9294
|
+
mutatesReceiver: fn.mutatesReceiver,
|
|
9295
|
+
mutatedParameterIndexes: [...fn.mutatedParameterIndexes].sort((a, b) => a - b),
|
|
9296
|
+
dependencies: [...fn.dependencies].sort(),
|
|
9297
|
+
mutationCalls: cachedMutationCalls(fn)
|
|
8432
9298
|
})));
|
|
8433
9299
|
analyzedModuleCount += 1;
|
|
8434
9300
|
if ((analyzedModuleCount & 31) === 0 || analyzedModuleCount === rawRecords.size) {
|
|
@@ -8442,7 +9308,13 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
8442
9308
|
const cacheModulesResult = {};
|
|
8443
9309
|
let assembledCount = 0;
|
|
8444
9310
|
for (const identity of identities) {
|
|
8445
|
-
const states = (analyzedFunctionStatesByModule.get(identity.id) ?? cachedFunctionStates.get(identity.id) ?? []).map((fn) => ({
|
|
9311
|
+
const states = (analyzedFunctionStatesByModule.get(identity.id) ?? cachedFunctionStates.get(identity.id) ?? []).map((fn) => ({
|
|
9312
|
+
...fn,
|
|
9313
|
+
mutatesReceiver: fn.mutatesReceiver ?? false,
|
|
9314
|
+
mutatedParameterIndexes: [...fn.mutatedParameterIndexes ?? []],
|
|
9315
|
+
dependencies: [...fn.dependencies],
|
|
9316
|
+
mutationCalls: [...fn.mutationCalls ?? []]
|
|
9317
|
+
}));
|
|
8446
9318
|
for (const fn of states)
|
|
8447
9319
|
functionStates.set(fn.id, fn);
|
|
8448
9320
|
const summary = summariesByModuleId.get(identity.id);
|
|
@@ -8459,6 +9331,7 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
8459
9331
|
onProgress?.({ phase: "assemble-graph", current: assembledCount, total: graphAssemblyTotal });
|
|
8460
9332
|
}
|
|
8461
9333
|
}
|
|
9334
|
+
propagateMutationEffects([...functionStates.values()]);
|
|
8462
9335
|
const reverseDependencies = new Map;
|
|
8463
9336
|
const functions = [...functionStates.values()];
|
|
8464
9337
|
onProgress?.({ phase: "resolve", current: 0, total: functions.length });
|
|
@@ -8505,17 +9378,33 @@ async function buildProjectSourceEffects(root, candidates, parseCache, onProgres
|
|
|
8505
9378
|
for (const summary of summariesByModuleId.values()) {
|
|
8506
9379
|
summary.effectfulMembers.clear();
|
|
8507
9380
|
summary.effectfulExport = false;
|
|
9381
|
+
summary.mutatingMembers.clear();
|
|
9382
|
+
summary.mutatingExportParameters.clear();
|
|
9383
|
+
summary.mutatingMemberParameters.clear();
|
|
9384
|
+
summary.localMutatingParameters.clear();
|
|
8508
9385
|
}
|
|
8509
9386
|
for (let index = 0;index < functions.length; index += 1) {
|
|
8510
9387
|
const fn = functions[index];
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
summary.
|
|
9388
|
+
const summary = summariesByModuleId.get(fn.moduleId);
|
|
9389
|
+
if (summary && fn.memberName && fn.mutatesReceiver)
|
|
9390
|
+
summary.mutatingMembers.add(fn.memberName);
|
|
9391
|
+
if (summary && fn.mutatedParameterIndexes.length > 0) {
|
|
9392
|
+
if (fn.exported && !fn.memberName) {
|
|
9393
|
+
for (const parameterIndex of fn.mutatedParameterIndexes)
|
|
9394
|
+
summary.mutatingExportParameters.add(parameterIndex);
|
|
9395
|
+
}
|
|
9396
|
+
if (fn.memberName) {
|
|
9397
|
+
summary.mutatingMemberParameters.set(fn.memberName, new Set(fn.mutatedParameterIndexes));
|
|
8518
9398
|
}
|
|
9399
|
+
if (fn.localName) {
|
|
9400
|
+
summary.localMutatingParameters.set(fn.localName, new Set(fn.mutatedParameterIndexes));
|
|
9401
|
+
}
|
|
9402
|
+
}
|
|
9403
|
+
if (summary && effectful.has(fn.id)) {
|
|
9404
|
+
if (fn.memberName)
|
|
9405
|
+
summary.effectfulMembers.add(fn.memberName);
|
|
9406
|
+
if (fn.exported)
|
|
9407
|
+
summary.effectfulExport = true;
|
|
8519
9408
|
}
|
|
8520
9409
|
if ((index & 127) === 127 || index + 1 === functions.length) {
|
|
8521
9410
|
onProgress?.({ phase: "summarize", current: index + 1, total: summarizeTotal });
|
|
@@ -11590,5 +12479,5 @@ ${os2.release()}
|
|
|
11590
12479
|
}
|
|
11591
12480
|
main();
|
|
11592
12481
|
|
|
11593
|
-
//# debugId=
|
|
12482
|
+
//# debugId=77F2455CC417160A64756E2164756E21
|
|
11594
12483
|
//# sourceMappingURL=cli.js.map
|