@colrealpro/react-luau-doctor 0.17.5 → 0.18.0

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