@colrealpro/react-luau-doctor 0.18.3 → 0.19.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/CHANGELOG.md +14 -0
- package/dist/cli.js +952 -135
- package/dist/cli.js.map +11 -11
- package/dist/scan-worker.js +662 -107
- package/dist/scan-worker.js.map +6 -6
- package/package.json +2 -1
package/dist/scan-worker.js
CHANGED
|
@@ -1433,6 +1433,8 @@ function hasRobloxReceiver(path3, call, context) {
|
|
|
1433
1433
|
}
|
|
1434
1434
|
var runServiceCallCache = new WeakMap;
|
|
1435
1435
|
var runServiceExpressionCache = new WeakMap;
|
|
1436
|
+
var highFrequencyRobloxCallCache = new WeakMap;
|
|
1437
|
+
var highFrequencyRobloxExpressionCache = new WeakMap;
|
|
1436
1438
|
function cachedNodeBoolean(cache, context, node, compute) {
|
|
1437
1439
|
let entries = cache.get(context);
|
|
1438
1440
|
if (!entries) {
|
|
@@ -1497,6 +1499,33 @@ function isHighFrequencyRunServiceCall(call, context) {
|
|
|
1497
1499
|
return Boolean(bind && rootIsRunService(bind[1], call, context));
|
|
1498
1500
|
});
|
|
1499
1501
|
}
|
|
1502
|
+
var HIGH_FREQUENCY_ROBLOX_EVENTS = new Set([
|
|
1503
|
+
"InputChanged",
|
|
1504
|
+
"TouchMoved",
|
|
1505
|
+
"Move"
|
|
1506
|
+
]);
|
|
1507
|
+
function isHighFrequencyRobloxExpression(node, context) {
|
|
1508
|
+
return cachedNodeBoolean(highFrequencyRobloxExpressionCache, context, node, () => {
|
|
1509
|
+
if (isHighFrequencyRunServiceExpression(node, context))
|
|
1510
|
+
return true;
|
|
1511
|
+
const normalized = node.text.replace(/\s+/g, "");
|
|
1512
|
+
const match = normalized.match(/^(.+)\.(InputChanged|TouchMoved|Move)$/);
|
|
1513
|
+
if (!match || !HIGH_FREQUENCY_ROBLOX_EVENTS.has(match[2]))
|
|
1514
|
+
return false;
|
|
1515
|
+
return hasRobloxReceiver(match[1], node, context);
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
function isHighFrequencyRobloxCall(call, context) {
|
|
1519
|
+
return cachedNodeBoolean(highFrequencyRobloxCallCache, context, call, () => {
|
|
1520
|
+
if (isHighFrequencyRunServiceCall(call, context))
|
|
1521
|
+
return true;
|
|
1522
|
+
const normalized = (context.getCallPath(call) ?? "").replace(/\s+/g, "");
|
|
1523
|
+
const event = normalized.match(/^(.+)\.(InputChanged|TouchMoved|Move):Connect$/);
|
|
1524
|
+
if (!event || !HIGH_FREQUENCY_ROBLOX_EVENTS.has(event[2]))
|
|
1525
|
+
return false;
|
|
1526
|
+
return hasRobloxReceiver(event[1], call, context);
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1500
1529
|
function sourceHasHighFrequencyRunService(source) {
|
|
1501
1530
|
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)) {
|
|
1502
1531
|
return true;
|
|
@@ -1513,6 +1542,36 @@ function sourceHasHighFrequencyRunService(source) {
|
|
|
1513
1542
|
}
|
|
1514
1543
|
return false;
|
|
1515
1544
|
}
|
|
1545
|
+
function sourceHasHighFrequencyRobloxEvent(source) {
|
|
1546
|
+
if (sourceHasHighFrequencyRunService(source))
|
|
1547
|
+
return true;
|
|
1548
|
+
if (/game\s*:\s*GetService\s*\(\s*["']UserInputService["']\s*\)\s*\.\s*(?:InputChanged|TouchMoved)\s*:\s*Connect/.test(source)) {
|
|
1549
|
+
return true;
|
|
1550
|
+
}
|
|
1551
|
+
const serviceAliases = new Set;
|
|
1552
|
+
for (const match of source.matchAll(/\blocal\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*game\s*:\s*GetService\s*\(\s*["']UserInputService["']\s*\)/g)) {
|
|
1553
|
+
serviceAliases.add(match[1]);
|
|
1554
|
+
}
|
|
1555
|
+
for (const alias of serviceAliases) {
|
|
1556
|
+
const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1557
|
+
if (new RegExp(`\\b${escaped}\\s*\\.\\s*(?:InputChanged|TouchMoved)\\s*:\\s*Connect`).test(source)) {
|
|
1558
|
+
return true;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
const playersAliases = new Set;
|
|
1562
|
+
for (const match of source.matchAll(/\blocal\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*game\s*:\s*GetService\s*\(\s*["']Players["']\s*\)/g)) {
|
|
1563
|
+
playersAliases.add(match[1]);
|
|
1564
|
+
}
|
|
1565
|
+
for (const players of playersAliases) {
|
|
1566
|
+
const escapedPlayers = players.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1567
|
+
for (const match of source.matchAll(new RegExp(`\\blocal\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*${escapedPlayers}\\s*\\.\\s*LocalPlayer\\s*:\\s*GetMouse\\s*\\(\\s*\\)`, "g"))) {
|
|
1568
|
+
const mouse = match[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1569
|
+
if (new RegExp(`\\b${mouse}\\s*\\.\\s*Move\\s*:\\s*Connect`).test(source))
|
|
1570
|
+
return true;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1516
1575
|
function knownYieldReason(path3, call, context) {
|
|
1517
1576
|
const normalized = path3.replace(/\s+/g, "");
|
|
1518
1577
|
if (normalized === "task.wait")
|
|
@@ -4059,7 +4118,7 @@ var rerenderHighFrequencyState = {
|
|
|
4059
4118
|
seen.add(key);
|
|
4060
4119
|
const renderCost = componentRenderCost(context, component);
|
|
4061
4120
|
diagnostics.push({
|
|
4062
|
-
node: callNameNode(call),
|
|
4121
|
+
node: callNameNode(binding.call),
|
|
4063
4122
|
severity: "warning",
|
|
4064
4123
|
message: `${binding.valueName} has a state setter called from a per-frame callback and may cause frequent rerenders when its value changes.`,
|
|
4065
4124
|
help: renderCost.length > 0 ? `This render also performs ${renderCost.join(" and ")}. Isolate or throttle the high-frequency state, or use Bindings for host-property-only updates.` : "Isolate or throttle the high-frequency state. Use React.useBinding when the value only drives host Instance properties."
|
|
@@ -4336,7 +4395,9 @@ var preferUseRefForMutableCell = {
|
|
|
4336
4395
|
};
|
|
4337
4396
|
|
|
4338
4397
|
// src/rules/prefer-binding-over-state.ts
|
|
4339
|
-
var EXTERNAL_CALLBACK = /(?:^|[.:])(?:Connect|
|
|
4398
|
+
var EXTERNAL_CALLBACK = /(?:^|[.:])(?:Connect|Subscribe|Observe|Listen|Watch|onStep|onUpdate|onChange|onChanged)$/i;
|
|
4399
|
+
var PERSISTENT_MIRROR_CALLBACK = /(?:^|[.:])(?:Connect|Subscribe|Observe|Listen|Watch|onStep|onUpdate|onChange|onChanged)$/i;
|
|
4400
|
+
var LIKELY_CONTINUOUS_CALLBACK = /(?:^|[.:])onStep$/i;
|
|
4340
4401
|
var NON_BINDABLE_HOST_FIELDS = new Set(["ref", "key", "children"]);
|
|
4341
4402
|
function replaceWithinNode(container, target, replacement) {
|
|
4342
4403
|
const start = target.startIndex - container.startIndex;
|
|
@@ -4459,13 +4520,64 @@ var TRANSPARENT_PARENTS = new Set([
|
|
|
4459
4520
|
"string_interpolation",
|
|
4460
4521
|
"expression_list",
|
|
4461
4522
|
"assignment_statement",
|
|
4462
|
-
"arguments",
|
|
4463
|
-
"function_call",
|
|
4464
4523
|
"dot_index_expression",
|
|
4465
4524
|
"method_index_expression",
|
|
4466
4525
|
"table_constructor",
|
|
4467
4526
|
"field"
|
|
4468
4527
|
]);
|
|
4528
|
+
var PURE_BINDING_DERIVATION_CALL = /^(?:tostring|tonumber|type|typeof|math\.[A-Za-z_][A-Za-z0-9_]*|string\.[A-Za-z_][A-Za-z0-9_]*|utf8\.[A-Za-z_][A-Za-z0-9_]*|bit32\.[A-Za-z_][A-Za-z0-9_]*|(?:UDim|UDim2|Vector2|Vector3|Color3|CFrame|Rect|NumberRange|NumberSequence|ColorSequence|BrickColor|Font)\.[A-Za-z_][A-Za-z0-9_]*)$/;
|
|
4529
|
+
var HOST_FEEDBACK_PROPERTIES = new Set([
|
|
4530
|
+
"AbsoluteSize",
|
|
4531
|
+
"AbsolutePosition",
|
|
4532
|
+
"AbsoluteRotation",
|
|
4533
|
+
"AbsoluteContentSize",
|
|
4534
|
+
"AbsoluteCanvasSize",
|
|
4535
|
+
"AbsoluteCellSize",
|
|
4536
|
+
"AbsoluteCellCount",
|
|
4537
|
+
"TextBounds",
|
|
4538
|
+
"CanvasPosition"
|
|
4539
|
+
]);
|
|
4540
|
+
var LIKELY_CONTINUOUS_PROPERTIES = new Set([
|
|
4541
|
+
"CFrame",
|
|
4542
|
+
"WorldCFrame",
|
|
4543
|
+
"Position",
|
|
4544
|
+
"WorldPosition",
|
|
4545
|
+
"Rotation",
|
|
4546
|
+
"Orientation",
|
|
4547
|
+
"CanvasPosition",
|
|
4548
|
+
"PlaybackLoudness",
|
|
4549
|
+
"TimePosition"
|
|
4550
|
+
]);
|
|
4551
|
+
var SEMANTIC_SNAPSHOT_EXPRESSION = /\b(?:getState|getSnapshot|snapshot|selector|select|table\s*\.\s*(?:clone|freeze|move))\b|(?:\.|\[\s*["'])State(?:\b|["']\s*\])/i;
|
|
4552
|
+
function isBindingMapSafeCall(call, context) {
|
|
4553
|
+
if (call.type !== "function_call")
|
|
4554
|
+
return false;
|
|
4555
|
+
const path4 = context.resolveCallPath(context.getCallPath(call) ?? "").replace(/\s+/g, "");
|
|
4556
|
+
return PURE_BINDING_DERIVATION_CALL.test(path4);
|
|
4557
|
+
}
|
|
4558
|
+
function isTransparentParent(parent, context) {
|
|
4559
|
+
if (parent.type === "function_call")
|
|
4560
|
+
return isBindingMapSafeCall(parent, context);
|
|
4561
|
+
if (parent.type === "arguments") {
|
|
4562
|
+
return parent.parent?.type === "function_call" && isBindingMapSafeCall(parent.parent, context);
|
|
4563
|
+
}
|
|
4564
|
+
return TRANSPARENT_PARENTS.has(parent.type);
|
|
4565
|
+
}
|
|
4566
|
+
function stringLiteralValue(node) {
|
|
4567
|
+
if (!node)
|
|
4568
|
+
return null;
|
|
4569
|
+
return node.text.trim().match(/^["']([^"']+)["']$/)?.[1] ?? null;
|
|
4570
|
+
}
|
|
4571
|
+
function observedPropertyForCall(summary, call, context) {
|
|
4572
|
+
if (summary.observedPropertyName)
|
|
4573
|
+
return summary.observedPropertyName;
|
|
4574
|
+
if (summary.observedPropertyParameterIndex === undefined)
|
|
4575
|
+
return null;
|
|
4576
|
+
return stringLiteralValue(context.callArguments(call)[summary.observedPropertyParameterIndex]);
|
|
4577
|
+
}
|
|
4578
|
+
function isStrongHostFeedbackProperty(property) {
|
|
4579
|
+
return Boolean(property && HOST_FEEDBACK_PROPERTIES.has(property));
|
|
4580
|
+
}
|
|
4469
4581
|
function isInside(node, ancestor) {
|
|
4470
4582
|
let current = node;
|
|
4471
4583
|
while (current) {
|
|
@@ -4549,13 +4661,75 @@ function isTransparentUsage(node, context, compatibleComponentProps) {
|
|
|
4549
4661
|
}
|
|
4550
4662
|
if (parent.type === "function_definition" || parent.type === "function_declaration")
|
|
4551
4663
|
return false;
|
|
4552
|
-
if (!
|
|
4664
|
+
if (!isTransparentParent(parent, context))
|
|
4553
4665
|
return false;
|
|
4554
4666
|
current = parent;
|
|
4555
4667
|
}
|
|
4556
4668
|
return false;
|
|
4557
4669
|
}
|
|
4558
|
-
function
|
|
4670
|
+
function isDirectBindingValueUsage(node, context, compatibleComponentProps) {
|
|
4671
|
+
let current = node;
|
|
4672
|
+
while (current) {
|
|
4673
|
+
const parent = current.parent;
|
|
4674
|
+
if (!parent)
|
|
4675
|
+
return false;
|
|
4676
|
+
if (parent.type === "parenthesized_expression" || parent.type === "cast_expression" || parent.type === "type_cast_expression") {
|
|
4677
|
+
current = parent;
|
|
4678
|
+
continue;
|
|
4679
|
+
}
|
|
4680
|
+
if (parent.type !== "field")
|
|
4681
|
+
return false;
|
|
4682
|
+
const value = fieldValue(parent);
|
|
4683
|
+
if (!value || !sameNode(value, current))
|
|
4684
|
+
return false;
|
|
4685
|
+
const property = fieldName(parent);
|
|
4686
|
+
if (!property || NON_BINDABLE_HOST_FIELDS.has(property))
|
|
4687
|
+
return false;
|
|
4688
|
+
const table = parent.parent;
|
|
4689
|
+
const args = table?.parent;
|
|
4690
|
+
const call = args?.parent;
|
|
4691
|
+
if (table?.type !== "table_constructor" || args?.type !== "arguments" || !call || !isCreateElementCall2(call, context)) {
|
|
4692
|
+
return false;
|
|
4693
|
+
}
|
|
4694
|
+
if (!sameNode(context.callArguments(call)[1], table))
|
|
4695
|
+
return false;
|
|
4696
|
+
if (isHostCreateElementCall(call, context))
|
|
4697
|
+
return true;
|
|
4698
|
+
const component = context.callArguments(call)[0]?.text.trim() ?? "";
|
|
4699
|
+
return compatibleComponentProps.get(component)?.has(property) ?? false;
|
|
4700
|
+
}
|
|
4701
|
+
return false;
|
|
4702
|
+
}
|
|
4703
|
+
function allReadsDirectlyBindingSubstitutable(valueName, owner, declaration, context, compatibleComponentProps) {
|
|
4704
|
+
if (!owner.body)
|
|
4705
|
+
return false;
|
|
4706
|
+
let reads = 0;
|
|
4707
|
+
for (const node of context.walk(owner.body)) {
|
|
4708
|
+
if (!isReadNode(node, valueName, owner, declaration))
|
|
4709
|
+
continue;
|
|
4710
|
+
reads += 1;
|
|
4711
|
+
if (!isDirectBindingValueUsage(node, context, compatibleComponentProps))
|
|
4712
|
+
return false;
|
|
4713
|
+
}
|
|
4714
|
+
return reads > 0;
|
|
4715
|
+
}
|
|
4716
|
+
function derivedLocalDeclarationForRead(node, context) {
|
|
4717
|
+
let current = node;
|
|
4718
|
+
while (current) {
|
|
4719
|
+
const parent = current.parent;
|
|
4720
|
+
if (!parent)
|
|
4721
|
+
return null;
|
|
4722
|
+
if (parent.type === "variable_declaration")
|
|
4723
|
+
return parent;
|
|
4724
|
+
if (parent.type === "function_definition" || parent.type === "function_declaration")
|
|
4725
|
+
return null;
|
|
4726
|
+
if (!isTransparentParent(parent, context))
|
|
4727
|
+
return null;
|
|
4728
|
+
current = parent;
|
|
4729
|
+
}
|
|
4730
|
+
return null;
|
|
4731
|
+
}
|
|
4732
|
+
function opaqueDerivedLocalDeclarationForRead(node, context) {
|
|
4559
4733
|
let current = node;
|
|
4560
4734
|
while (current) {
|
|
4561
4735
|
const parent = current.parent;
|
|
@@ -4565,7 +4739,14 @@ function derivedLocalDeclarationForRead(node) {
|
|
|
4565
4739
|
return parent;
|
|
4566
4740
|
if (parent.type === "function_definition" || parent.type === "function_declaration")
|
|
4567
4741
|
return null;
|
|
4568
|
-
if (
|
|
4742
|
+
if (parent.type === "if_statement" || parent.type === "elseif_clause" || parent.type === "while_statement" || parent.type === "repeat_statement" || parent.type === "for_statement" || parent.type === "for_in_statement" || parent.type === "return_statement")
|
|
4743
|
+
return null;
|
|
4744
|
+
if (parent.type === "function_call") {
|
|
4745
|
+
const path4 = context.resolveCallPath(context.getCallPath(parent) ?? "");
|
|
4746
|
+
if (/^React\.use(?:Memo|Callback|Effect|LayoutEffect)$/.test(path4))
|
|
4747
|
+
return null;
|
|
4748
|
+
}
|
|
4749
|
+
if (!TRANSPARENT_PARENTS.has(parent.type) && parent.type !== "arguments" && parent.type !== "function_call")
|
|
4569
4750
|
return null;
|
|
4570
4751
|
current = parent;
|
|
4571
4752
|
}
|
|
@@ -4585,7 +4766,7 @@ function stateReadsAreBindingCompatible(valueName, owner, declaration, context,
|
|
|
4585
4766
|
reads += 1;
|
|
4586
4767
|
if (isTransparentUsage(node, context, compatibleComponentProps))
|
|
4587
4768
|
continue;
|
|
4588
|
-
const derivedDeclaration = derivedLocalDeclarationForRead(node);
|
|
4769
|
+
const derivedDeclaration = derivedLocalDeclarationForRead(node, context);
|
|
4589
4770
|
const derivedNames = derivedDeclaration ? declarationNames2(derivedDeclaration) : [];
|
|
4590
4771
|
if (derivedDeclaration && derivedNames.length === 1) {
|
|
4591
4772
|
if (stateReadsAreBindingCompatible(derivedNames[0], owner, derivedDeclaration, context, compatibleComponentProps, new Set(visited)))
|
|
@@ -4595,12 +4776,46 @@ function stateReadsAreBindingCompatible(valueName, owner, declaration, context,
|
|
|
4595
4776
|
}
|
|
4596
4777
|
return reads > 0;
|
|
4597
4778
|
}
|
|
4779
|
+
function propertyChangedSignalProperty(text) {
|
|
4780
|
+
return text.match(/GetPropertyChangedSignal\s*\(\s*["']([^"']+)["']\s*\)/)?.[1] ?? null;
|
|
4781
|
+
}
|
|
4782
|
+
function reactChangePropertyForCallback(node) {
|
|
4783
|
+
let current = node.parent;
|
|
4784
|
+
while (current && current.type !== "function_definition" && current.type !== "function_declaration") {
|
|
4785
|
+
if (current.type === "field") {
|
|
4786
|
+
return current.text.match(/React\s*\.\s*Change\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)/)?.[1] ?? null;
|
|
4787
|
+
}
|
|
4788
|
+
current = current.parent;
|
|
4789
|
+
}
|
|
4790
|
+
return null;
|
|
4791
|
+
}
|
|
4792
|
+
function evidenceForExternalSource(highFrequency, hostFeedbackProperty) {
|
|
4793
|
+
const hostFeedbackProperties = new Set;
|
|
4794
|
+
if (hostFeedbackProperty)
|
|
4795
|
+
hostFeedbackProperties.add(hostFeedbackProperty);
|
|
4796
|
+
return {
|
|
4797
|
+
highFrequency,
|
|
4798
|
+
external: true,
|
|
4799
|
+
hostFeedbackProperties,
|
|
4800
|
+
genericExternal: !highFrequency && !hostFeedbackProperty
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
function mergeSourceEvidence(target, source) {
|
|
4804
|
+
target.highFrequency ||= source.highFrequency;
|
|
4805
|
+
target.external ||= source.external;
|
|
4806
|
+
target.genericExternal ||= source.genericExternal;
|
|
4807
|
+
for (const property of source.hostFeedbackProperties)
|
|
4808
|
+
target.hostFeedbackProperties.add(property);
|
|
4809
|
+
}
|
|
4598
4810
|
function callbackSource(call, context) {
|
|
4599
4811
|
const normalized = (context.getCallPath(call) ?? "").replace(/\s+/g, "");
|
|
4600
|
-
const highFrequency =
|
|
4812
|
+
const highFrequency = isHighFrequencyRobloxCall(call, context) || LIKELY_CONTINUOUS_CALLBACK.test(normalized);
|
|
4813
|
+
const hostFeedbackProperty = propertyChangedSignalProperty(call.text);
|
|
4601
4814
|
const final = normalized.split(/[.:]/).at(-1) ?? normalized;
|
|
4602
|
-
const external = highFrequency || EXTERNAL_CALLBACK.test(normalized) || EXTERNAL_CALLBACK.test(final);
|
|
4603
|
-
|
|
4815
|
+
const external = highFrequency || Boolean(hostFeedbackProperty) || EXTERNAL_CALLBACK.test(normalized) || EXTERNAL_CALLBACK.test(final);
|
|
4816
|
+
if (!external)
|
|
4817
|
+
return null;
|
|
4818
|
+
return evidenceForExternalSource(highFrequency, hostFeedbackProperty);
|
|
4604
4819
|
}
|
|
4605
4820
|
function importedCallbackSource(call, callbackArgument, importedCallbacks, context) {
|
|
4606
4821
|
const path4 = context.getCallPath(call) ?? "";
|
|
@@ -4611,12 +4826,14 @@ function importedCallbackSource(call, callbackArgument, importedCallbacks, conte
|
|
|
4611
4826
|
const callbackIndex = args.findIndex((arg) => sameNode(arg, callbackArgument));
|
|
4612
4827
|
if (callbackIndex < 0 || !summary.callbackParameterIndexes.includes(callbackIndex))
|
|
4613
4828
|
return null;
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
};
|
|
4829
|
+
const highFrequency = summary.highFrequency || args.some((arg) => isHighFrequencyRobloxExpression(arg, context));
|
|
4830
|
+
const hostFeedbackProperty = args.map((arg) => propertyChangedSignalProperty(arg.text)).find((property) => Boolean(property));
|
|
4831
|
+
return evidenceForExternalSource(highFrequency, hostFeedbackProperty);
|
|
4618
4832
|
}
|
|
4619
4833
|
function directCallbackSource(node, importedCallbacks, context) {
|
|
4834
|
+
const reactChangeProperty = reactChangePropertyForCallback(node);
|
|
4835
|
+
if (reactChangeProperty)
|
|
4836
|
+
return evidenceForExternalSource(false, reactChangeProperty);
|
|
4620
4837
|
const parent = node.parent;
|
|
4621
4838
|
if (parent?.type !== "arguments" || parent.parent?.type !== "function_call")
|
|
4622
4839
|
return null;
|
|
@@ -4624,14 +4841,17 @@ function directCallbackSource(node, importedCallbacks, context) {
|
|
|
4624
4841
|
const imported = importedCallbackSource(call, node, importedCallbacks, context);
|
|
4625
4842
|
if (imported)
|
|
4626
4843
|
return imported;
|
|
4627
|
-
|
|
4628
|
-
return source.external ? source : null;
|
|
4844
|
+
return callbackSource(call, context);
|
|
4629
4845
|
}
|
|
4630
4846
|
function namedFunctionCallbackSource(fn, owner, importedCallbacks, context) {
|
|
4631
4847
|
if (!fn.name || !owner.body)
|
|
4632
4848
|
return null;
|
|
4633
|
-
|
|
4634
|
-
|
|
4849
|
+
const result = {
|
|
4850
|
+
highFrequency: false,
|
|
4851
|
+
external: false,
|
|
4852
|
+
hostFeedbackProperties: new Set,
|
|
4853
|
+
genericExternal: false
|
|
4854
|
+
};
|
|
4635
4855
|
for (const call of context.walk(owner.body)) {
|
|
4636
4856
|
if (call.type !== "function_call")
|
|
4637
4857
|
continue;
|
|
@@ -4639,14 +4859,12 @@ function namedFunctionCallbackSource(fn, owner, importedCallbacks, context) {
|
|
|
4639
4859
|
const callbackArg = args.find((arg) => arg.type === "identifier" && arg.text === fn.name);
|
|
4640
4860
|
if (!callbackArg)
|
|
4641
4861
|
continue;
|
|
4642
|
-
const
|
|
4643
|
-
|
|
4644
|
-
if (!source.external)
|
|
4862
|
+
const source = importedCallbackSource(call, callbackArg, importedCallbacks, context) ?? callbackSource(call, context);
|
|
4863
|
+
if (!source)
|
|
4645
4864
|
continue;
|
|
4646
|
-
|
|
4647
|
-
foundHighFrequency ||= source.highFrequency;
|
|
4865
|
+
mergeSourceEvidence(result, source);
|
|
4648
4866
|
}
|
|
4649
|
-
return
|
|
4867
|
+
return result.external ? result : null;
|
|
4650
4868
|
}
|
|
4651
4869
|
function setterCallbackSource(call, binding, importedCallbacks, context) {
|
|
4652
4870
|
const owner = binding.owner;
|
|
@@ -4660,25 +4878,178 @@ function setterCallbackSource(call, binding, importedCallbacks, context) {
|
|
|
4660
4878
|
return direct;
|
|
4661
4879
|
return namedFunctionCallbackSource(nearest, owner, importedCallbacks, context);
|
|
4662
4880
|
}
|
|
4663
|
-
function
|
|
4881
|
+
function mirrorConfidenceRank(confidence) {
|
|
4882
|
+
if (confidence === "strong")
|
|
4883
|
+
return 2;
|
|
4884
|
+
if (confidence === "possible")
|
|
4885
|
+
return 1;
|
|
4886
|
+
return 0;
|
|
4887
|
+
}
|
|
4888
|
+
function mergeMirrorConfidence(a, b) {
|
|
4889
|
+
return mirrorConfidenceRank(b) > mirrorConfidenceRank(a) ? b : a;
|
|
4890
|
+
}
|
|
4891
|
+
function isLiteralStateTransition(node) {
|
|
4892
|
+
if (!node)
|
|
4893
|
+
return true;
|
|
4894
|
+
const text = node.text.trim();
|
|
4895
|
+
return /^(?:true|false|nil|[-+]?\d+(?:\.\d+)?|["'][^"']*["'])$/.test(text);
|
|
4896
|
+
}
|
|
4897
|
+
function isSemanticSnapshotExpression(node) {
|
|
4898
|
+
return Boolean(node && SEMANTIC_SNAPSHOT_EXPRESSION.test(node.text));
|
|
4899
|
+
}
|
|
4900
|
+
function expressionReferencesCallbackParameter(expression, callback, context) {
|
|
4901
|
+
if (callback.parameters.length === 0)
|
|
4902
|
+
return false;
|
|
4903
|
+
const parameters = new Set(callback.parameters);
|
|
4904
|
+
return [...context.walk(expression)].some((node) => node.type === "identifier" && parameters.has(node.text));
|
|
4905
|
+
}
|
|
4906
|
+
var PURE_EXPRESSION_ROOTS = new Set([
|
|
4907
|
+
"math",
|
|
4908
|
+
"string",
|
|
4909
|
+
"utf8",
|
|
4910
|
+
"bit32",
|
|
4911
|
+
"UDim",
|
|
4912
|
+
"UDim2",
|
|
4913
|
+
"Vector2",
|
|
4914
|
+
"Vector3",
|
|
4915
|
+
"Color3",
|
|
4916
|
+
"CFrame",
|
|
4917
|
+
"Rect",
|
|
4918
|
+
"NumberRange",
|
|
4919
|
+
"NumberSequence",
|
|
4920
|
+
"ColorSequence",
|
|
4921
|
+
"BrickColor",
|
|
4922
|
+
"Font",
|
|
4923
|
+
"Enum",
|
|
4924
|
+
"tostring",
|
|
4925
|
+
"tonumber",
|
|
4926
|
+
"type",
|
|
4927
|
+
"typeof"
|
|
4928
|
+
]);
|
|
4929
|
+
function callbackLocalNamesBefore(callback, before, context) {
|
|
4930
|
+
const names = new Set;
|
|
4931
|
+
if (!callback.body)
|
|
4932
|
+
return names;
|
|
4933
|
+
for (const node of context.walk(callback.body)) {
|
|
4934
|
+
if (node.startIndex >= before.startIndex)
|
|
4935
|
+
continue;
|
|
4936
|
+
if (context.nearestFunction(node) !== callback || node.type !== "variable_declaration")
|
|
4937
|
+
continue;
|
|
4938
|
+
for (const name of declarationNames2(node))
|
|
4939
|
+
names.add(name);
|
|
4940
|
+
}
|
|
4941
|
+
return names;
|
|
4942
|
+
}
|
|
4943
|
+
function expressionHasExternalInput(expression, callback, setterCall, context) {
|
|
4944
|
+
const callbackLocals = callbackLocalNamesBefore(callback, setterCall, context);
|
|
4945
|
+
const callbackParameters = new Set(callback.parameters);
|
|
4946
|
+
for (const node of context.walk(expression)) {
|
|
4947
|
+
if (node.type !== "identifier" || isIdentifierPropertyName2(node))
|
|
4948
|
+
continue;
|
|
4949
|
+
const name = node.text;
|
|
4950
|
+
if (callbackParameters.has(name))
|
|
4951
|
+
return true;
|
|
4952
|
+
if (callbackLocals.has(name))
|
|
4953
|
+
continue;
|
|
4954
|
+
if (context.model.stateValues.has(name))
|
|
4955
|
+
continue;
|
|
4956
|
+
if (PURE_EXPRESSION_ROOTS.has(name))
|
|
4957
|
+
continue;
|
|
4958
|
+
return true;
|
|
4959
|
+
}
|
|
4960
|
+
return false;
|
|
4961
|
+
}
|
|
4962
|
+
function setterHasDistinctValuesInCallback(call, callback, context) {
|
|
4963
|
+
if (!callback.body)
|
|
4964
|
+
return false;
|
|
4965
|
+
const setterPath = context.getCallPath(call);
|
|
4966
|
+
const distinctValues = new Set;
|
|
4967
|
+
for (const sibling of context.walk(callback.body)) {
|
|
4968
|
+
if (sibling.type !== "function_call" || context.nearestFunction(sibling) !== callback)
|
|
4969
|
+
continue;
|
|
4970
|
+
if (context.getCallPath(sibling) !== setterPath)
|
|
4971
|
+
continue;
|
|
4972
|
+
const argument = context.callArguments(sibling)[0];
|
|
4973
|
+
if (argument)
|
|
4974
|
+
distinctValues.add(argument.text.trim());
|
|
4975
|
+
}
|
|
4976
|
+
return distinctValues.size > 1;
|
|
4977
|
+
}
|
|
4978
|
+
function setterCallMirrorConfidence(call, callback, source, context) {
|
|
4979
|
+
const value = context.callArguments(call)[0];
|
|
4980
|
+
if (!value || value.type === "function_definition")
|
|
4981
|
+
return "none";
|
|
4982
|
+
if (isLiteralStateTransition(value)) {
|
|
4983
|
+
return source.highFrequency && setterHasDistinctValuesInCallback(call, callback, context) ? "strong" : "none";
|
|
4984
|
+
}
|
|
4985
|
+
if (isSemanticSnapshotExpression(value))
|
|
4986
|
+
return "none";
|
|
4987
|
+
if (expressionReferencesCallbackParameter(value, callback, context))
|
|
4988
|
+
return "strong";
|
|
4989
|
+
if (source.hostFeedbackProperties.size > 0) {
|
|
4990
|
+
const text = value.text;
|
|
4991
|
+
if ([...source.hostFeedbackProperties].some((property) => new RegExp(`(?:\\.|["'])${property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(text))) {
|
|
4992
|
+
return "strong";
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
if (!expressionHasExternalInput(value, callback, call, context)) {
|
|
4996
|
+
return source.highFrequency && setterHasDistinctValuesInCallback(call, callback, context) ? "strong" : "none";
|
|
4997
|
+
}
|
|
4998
|
+
if (source.highFrequency)
|
|
4999
|
+
return "strong";
|
|
5000
|
+
return source.genericExternal ? "possible" : "strong";
|
|
5001
|
+
}
|
|
5002
|
+
function mergeBindingUpdateEvidence(target, source, mirrorConfidence) {
|
|
5003
|
+
if (mirrorConfidence === "none")
|
|
5004
|
+
return;
|
|
5005
|
+
mergeSourceEvidence(target, source);
|
|
5006
|
+
target.mirrorConfidence = mergeMirrorConfidence(target.mirrorConfidence, mirrorConfidence);
|
|
5007
|
+
}
|
|
5008
|
+
function bindingUpdateEvidence(binding, importedCallbacks, context) {
|
|
4664
5009
|
const owner = binding.owner;
|
|
4665
5010
|
if (!owner?.body)
|
|
4666
5011
|
return null;
|
|
4667
|
-
|
|
4668
|
-
|
|
5012
|
+
const result = {
|
|
5013
|
+
highFrequency: false,
|
|
5014
|
+
external: false,
|
|
5015
|
+
hostFeedbackProperties: new Set,
|
|
5016
|
+
genericExternal: false,
|
|
5017
|
+
mirrorConfidence: "none"
|
|
5018
|
+
};
|
|
4669
5019
|
for (const node of context.walk(owner.body)) {
|
|
4670
5020
|
if (node.type !== "function_call")
|
|
4671
5021
|
continue;
|
|
4672
5022
|
const path4 = context.getCallPath(node);
|
|
4673
|
-
if (path4
|
|
5023
|
+
if (path4 === binding.setterName) {
|
|
5024
|
+
const source2 = setterCallbackSource(node, binding, importedCallbacks, context);
|
|
5025
|
+
const callback = context.nearestFunction(node);
|
|
5026
|
+
if (!source2 || !callback || callback === owner)
|
|
5027
|
+
continue;
|
|
5028
|
+
mergeBindingUpdateEvidence(result, source2, setterCallMirrorConfidence(node, callback, source2, context));
|
|
4674
5029
|
continue;
|
|
4675
|
-
|
|
5030
|
+
}
|
|
5031
|
+
const args = context.callArguments(node);
|
|
5032
|
+
const setterArg = args.find((arg) => arg.type === "identifier" && arg.text === binding.setterName);
|
|
5033
|
+
if (!setterArg)
|
|
5034
|
+
continue;
|
|
5035
|
+
const normalized = (context.getCallPath(node) ?? "").replace(/\s+/g, "");
|
|
5036
|
+
const imported = importedCallbackSource(node, setterArg, importedCallbacks, context);
|
|
5037
|
+
const source = imported ?? (PERSISTENT_MIRROR_CALLBACK.test(normalized) ? callbackSource(node, context) : null);
|
|
4676
5038
|
if (!source)
|
|
4677
5039
|
continue;
|
|
4678
|
-
|
|
4679
|
-
external ||= source.external;
|
|
5040
|
+
mergeBindingUpdateEvidence(result, source, "strong");
|
|
4680
5041
|
}
|
|
4681
|
-
return
|
|
5042
|
+
return result.external && result.mirrorConfidence !== "none" ? result : null;
|
|
5043
|
+
}
|
|
5044
|
+
function hasStrongHostFeedbackEvidence(source) {
|
|
5045
|
+
if (source.genericExternal || source.hostFeedbackProperties.size === 0)
|
|
5046
|
+
return false;
|
|
5047
|
+
return [...source.hostFeedbackProperties].every((property) => isStrongHostFeedbackProperty(property));
|
|
5048
|
+
}
|
|
5049
|
+
function hasLikelyContinuousPropertyEvidence(source) {
|
|
5050
|
+
if (source.hostFeedbackProperties.size === 0)
|
|
5051
|
+
return false;
|
|
5052
|
+
return [...source.hostFeedbackProperties].every((property) => LIKELY_CONTINUOUS_PROPERTIES.has(property));
|
|
4682
5053
|
}
|
|
4683
5054
|
function returnedHookValues(binding, context) {
|
|
4684
5055
|
const owner = binding.owner;
|
|
@@ -4756,14 +5127,8 @@ function callDefinitelyUsesBindingMode(summary, call, context) {
|
|
|
4756
5127
|
return false;
|
|
4757
5128
|
return truthy === summary.bindingWhenTruthy;
|
|
4758
5129
|
}
|
|
4759
|
-
function
|
|
4760
|
-
|
|
4761
|
-
return false;
|
|
4762
|
-
if (!hasAnyRead(localName, owner, declaration, context))
|
|
4763
|
-
return false;
|
|
4764
|
-
if (summary.sourceKind === "measurement")
|
|
4765
|
-
return true;
|
|
4766
|
-
return stateReadsAreBindingCompatible(localName, owner, declaration, context, compatibleComponentProps);
|
|
5130
|
+
function hasExplicitBindingMode(summary) {
|
|
5131
|
+
return summary.bindingModeParameterIndex !== undefined && summary.bindingWhenTruthy !== undefined;
|
|
4767
5132
|
}
|
|
4768
5133
|
var LIKELY_VISUAL_PROP = /(?:position|size|rotation|transparency|opacity|color|image|text|anchor|visible|enabled|layout|scale|offset|zindex|canvas|viewport|cframe|width|height|radius|stroke|padding|spacing|angle|alpha|progress)/i;
|
|
4769
5134
|
function createElementPropUsage(node, context) {
|
|
@@ -4792,7 +5157,7 @@ function createElementPropUsage(node, context) {
|
|
|
4792
5157
|
}
|
|
4793
5158
|
if (parent.type === "function_definition" || parent.type === "function_declaration")
|
|
4794
5159
|
return null;
|
|
4795
|
-
if (!
|
|
5160
|
+
if (!isTransparentParent(parent, context))
|
|
4796
5161
|
return null;
|
|
4797
5162
|
current = parent;
|
|
4798
5163
|
}
|
|
@@ -4824,17 +5189,17 @@ function isEffectUsage(node, context) {
|
|
|
4824
5189
|
}
|
|
4825
5190
|
return false;
|
|
4826
5191
|
}
|
|
4827
|
-
function isStructuralUsage(node) {
|
|
5192
|
+
function isStructuralUsage(node, context) {
|
|
4828
5193
|
let current = node;
|
|
4829
5194
|
while (current) {
|
|
4830
5195
|
const parent = current.parent;
|
|
4831
5196
|
if (!parent)
|
|
4832
5197
|
return false;
|
|
4833
|
-
if (parent.type === "if_statement" || parent.type === "elseif_clause" || parent.type === "while_statement" || parent.type === "repeat_statement" || parent.type === "return_statement")
|
|
5198
|
+
if (parent.type === "if_statement" || parent.type === "elseif_clause" || parent.type === "while_statement" || parent.type === "repeat_statement" || parent.type === "for_statement" || parent.type === "for_in_statement" || parent.type === "return_statement")
|
|
4834
5199
|
return true;
|
|
4835
5200
|
if (parent.type === "function_definition" || parent.type === "function_declaration")
|
|
4836
5201
|
return false;
|
|
4837
|
-
if (!
|
|
5202
|
+
if (!isTransparentParent(parent, context))
|
|
4838
5203
|
return false;
|
|
4839
5204
|
current = parent;
|
|
4840
5205
|
}
|
|
@@ -4847,11 +5212,11 @@ function classifyBindingUsage(node, context, compatibleComponentProps) {
|
|
|
4847
5212
|
return "custom-component";
|
|
4848
5213
|
if (isEffectUsage(node, context))
|
|
4849
5214
|
return "effect";
|
|
4850
|
-
if (isStructuralUsage(node))
|
|
5215
|
+
if (isStructuralUsage(node, context))
|
|
4851
5216
|
return "structural";
|
|
4852
5217
|
return "unknown";
|
|
4853
5218
|
}
|
|
4854
|
-
function bindingUsageSummary(valueName, owner, declaration, context, compatibleComponentProps) {
|
|
5219
|
+
function bindingUsageSummary(valueName, owner, declaration, context, compatibleComponentProps, visited = new Set) {
|
|
4855
5220
|
const summary = {
|
|
4856
5221
|
reads: 0,
|
|
4857
5222
|
compatible: 0,
|
|
@@ -4863,9 +5228,45 @@ function bindingUsageSummary(valueName, owner, declaration, context, compatibleC
|
|
|
4863
5228
|
};
|
|
4864
5229
|
if (!owner.body)
|
|
4865
5230
|
return summary;
|
|
5231
|
+
const visitKey = `${declaration.startIndex}:${valueName}`;
|
|
5232
|
+
if (visited.has(visitKey))
|
|
5233
|
+
return summary;
|
|
5234
|
+
visited.add(visitKey);
|
|
4866
5235
|
for (const node of context.walk(owner.body)) {
|
|
4867
5236
|
if (!isReadNode(node, valueName, owner, declaration))
|
|
4868
5237
|
continue;
|
|
5238
|
+
if (!isTransparentUsage(node, context, compatibleComponentProps)) {
|
|
5239
|
+
const derivedDeclaration = derivedLocalDeclarationForRead(node, context);
|
|
5240
|
+
const derivedNames = derivedDeclaration ? declarationNames2(derivedDeclaration) : [];
|
|
5241
|
+
if (derivedDeclaration && derivedNames.length === 1) {
|
|
5242
|
+
const derived = bindingUsageSummary(derivedNames[0], owner, derivedDeclaration, context, compatibleComponentProps, new Set(visited));
|
|
5243
|
+
if (derived.reads > 0) {
|
|
5244
|
+
summary.reads += derived.reads;
|
|
5245
|
+
summary.compatible += derived.compatible;
|
|
5246
|
+
summary.customComponent += derived.customComponent;
|
|
5247
|
+
summary.visualCustomComponent += derived.visualCustomComponent;
|
|
5248
|
+
summary.effect += derived.effect;
|
|
5249
|
+
summary.structural += derived.structural;
|
|
5250
|
+
summary.unknown += derived.unknown;
|
|
5251
|
+
continue;
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
const opaqueDeclaration = opaqueDerivedLocalDeclarationForRead(node, context);
|
|
5255
|
+
const opaqueNames = opaqueDeclaration ? declarationNames2(opaqueDeclaration) : [];
|
|
5256
|
+
if (opaqueDeclaration && opaqueNames.length === 1) {
|
|
5257
|
+
const derived = bindingUsageSummary(opaqueNames[0], owner, opaqueDeclaration, context, compatibleComponentProps, new Set(visited));
|
|
5258
|
+
if (derived.reads > 0) {
|
|
5259
|
+
summary.reads += derived.reads + 1;
|
|
5260
|
+
summary.compatible += derived.compatible;
|
|
5261
|
+
summary.customComponent += derived.customComponent;
|
|
5262
|
+
summary.visualCustomComponent += derived.visualCustomComponent;
|
|
5263
|
+
summary.effect += derived.effect;
|
|
5264
|
+
summary.structural += derived.structural;
|
|
5265
|
+
summary.unknown += derived.unknown + 1;
|
|
5266
|
+
continue;
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
4869
5270
|
summary.reads += 1;
|
|
4870
5271
|
const customComponentUsage = createElementPropUsage(node, context);
|
|
4871
5272
|
const kind = classifyBindingUsage(node, context, compatibleComponentProps);
|
|
@@ -4888,11 +5289,94 @@ function bindingUsageSummary(valueName, owner, declaration, context, compatibleC
|
|
|
4888
5289
|
function hasCandidatePresentationEvidence(summary) {
|
|
4889
5290
|
return summary.compatible > 0 || summary.visualCustomComponent > 0;
|
|
4890
5291
|
}
|
|
5292
|
+
function isPresentationOnly(summary) {
|
|
5293
|
+
return summary.reads > 0 && summary.compatible === summary.reads;
|
|
5294
|
+
}
|
|
5295
|
+
function stateBindingForSetter(setterName, context) {
|
|
5296
|
+
const valueName = context.model.stateSetters.get(setterName);
|
|
5297
|
+
if (!valueName)
|
|
5298
|
+
return null;
|
|
5299
|
+
return context.model.stateBindings.find((binding) => binding.setterName === setterName && binding.valueName === valueName) ?? null;
|
|
5300
|
+
}
|
|
5301
|
+
function effectOnlyDerivesPresentation(valueName, owner, declaration, context, compatibleComponentProps) {
|
|
5302
|
+
if (!owner.body)
|
|
5303
|
+
return false;
|
|
5304
|
+
let foundRelevantEffect = false;
|
|
5305
|
+
for (const call of context.walk(owner.body)) {
|
|
5306
|
+
if (call.type !== "function_call" || context.nearestFunction(call) !== owner)
|
|
5307
|
+
continue;
|
|
5308
|
+
const path4 = context.resolveCallPath(context.getCallPath(call) ?? "");
|
|
5309
|
+
if (path4 !== "React.useEffect" && path4 !== "React.useLayoutEffect")
|
|
5310
|
+
continue;
|
|
5311
|
+
const callbackNode = context.callArguments(call)[0];
|
|
5312
|
+
if (callbackNode?.type !== "function_definition")
|
|
5313
|
+
continue;
|
|
5314
|
+
const callback = context.model.functionByNode.get(callbackNode.id);
|
|
5315
|
+
if (!callback?.body)
|
|
5316
|
+
continue;
|
|
5317
|
+
const usesValue = [...context.walk(callback.body)].some((node) => context.nearestFunction(node) === callback && isReadNode(node, valueName, owner, declaration));
|
|
5318
|
+
if (!usesValue)
|
|
5319
|
+
continue;
|
|
5320
|
+
foundRelevantEffect = true;
|
|
5321
|
+
let presentationSetter = false;
|
|
5322
|
+
for (const nested of context.walk(callback.body)) {
|
|
5323
|
+
if (nested.type !== "function_call" || context.nearestFunction(nested) !== callback)
|
|
5324
|
+
continue;
|
|
5325
|
+
if (isBindingMapSafeCall(nested, context))
|
|
5326
|
+
continue;
|
|
5327
|
+
const nestedPath = context.resolveCallPath(context.getCallPath(nested) ?? "");
|
|
5328
|
+
if (nestedPath === "debug.profilebegin" || nestedPath === "debug.profileend")
|
|
5329
|
+
continue;
|
|
5330
|
+
if (context.model.bindingSetters.has(nestedPath)) {
|
|
5331
|
+
presentationSetter = true;
|
|
5332
|
+
continue;
|
|
5333
|
+
}
|
|
5334
|
+
const target = stateBindingForSetter(nestedPath, context);
|
|
5335
|
+
if (target?.owner && stateReadsAreBindingCompatible(target.valueName, target.owner, target.declaration, context, compatibleComponentProps)) {
|
|
5336
|
+
presentationSetter = true;
|
|
5337
|
+
continue;
|
|
5338
|
+
}
|
|
5339
|
+
return false;
|
|
5340
|
+
}
|
|
5341
|
+
if (!presentationSetter)
|
|
5342
|
+
return false;
|
|
5343
|
+
}
|
|
5344
|
+
return foundRelevantEffect;
|
|
5345
|
+
}
|
|
5346
|
+
function importedHookRecommendation(summary, call, localName, owner, declaration, context, compatibleComponentProps) {
|
|
5347
|
+
if (!summary.external || callDefinitelyUsesBindingMode(summary, call, context))
|
|
5348
|
+
return null;
|
|
5349
|
+
const usage = bindingUsageSummary(localName, owner, declaration, context, compatibleComponentProps);
|
|
5350
|
+
if (usage.reads === 0)
|
|
5351
|
+
return null;
|
|
5352
|
+
const presentationOnly = isPresentationOnly(usage);
|
|
5353
|
+
const explicitBindingMode = hasExplicitBindingMode(summary);
|
|
5354
|
+
const observedProperty = observedPropertyForCall(summary, call, context);
|
|
5355
|
+
const strongHostFeedback = summary.sourceKind === "instance-property" && isStrongHostFeedbackProperty(observedProperty);
|
|
5356
|
+
const likelyContinuousProperty = summary.sourceKind === "instance-property" && Boolean(observedProperty && LIKELY_CONTINUOUS_PROPERTIES.has(observedProperty));
|
|
5357
|
+
const mirrorsExternalValue = summary.mirrorConfidence !== "none";
|
|
5358
|
+
if (presentationOnly && (summary.highFrequency || likelyContinuousProperty))
|
|
5359
|
+
return "warning";
|
|
5360
|
+
if (presentationOnly && strongHostFeedback && explicitBindingMode)
|
|
5361
|
+
return "warning";
|
|
5362
|
+
if (presentationOnly && (explicitBindingMode || strongHostFeedback || mirrorsExternalValue))
|
|
5363
|
+
return "candidate";
|
|
5364
|
+
if (strongHostFeedback && usage.effect > 0 && effectOnlyDerivesPresentation(localName, owner, declaration, context, compatibleComponentProps)) {
|
|
5365
|
+
return "candidate";
|
|
5366
|
+
}
|
|
5367
|
+
if (!hasCandidatePresentationEvidence(usage))
|
|
5368
|
+
return null;
|
|
5369
|
+
if (summary.highFrequency || likelyContinuousProperty)
|
|
5370
|
+
return "candidate";
|
|
5371
|
+
if (strongHostFeedback && explicitBindingMode && usage.effect === 0)
|
|
5372
|
+
return "candidate";
|
|
5373
|
+
return null;
|
|
5374
|
+
}
|
|
4891
5375
|
var preferBindingOverState = {
|
|
4892
5376
|
id: "react-luau/prefer-binding-over-state",
|
|
4893
5377
|
category: "Performance",
|
|
4894
5378
|
severity: "warning",
|
|
4895
|
-
description: "Prefer React.useBinding when
|
|
5379
|
+
description: "Prefer React.useBinding when proven external presentation streams do not need React reconciliation.",
|
|
4896
5380
|
run(context) {
|
|
4897
5381
|
const diagnostics = [];
|
|
4898
5382
|
const seen = new Set;
|
|
@@ -4909,75 +5393,72 @@ var preferBindingOverState = {
|
|
|
4909
5393
|
const owner = context.containingComponent(call) ?? context.nearestFunction(call);
|
|
4910
5394
|
if (!declaration || !localName || !owner)
|
|
4911
5395
|
continue;
|
|
4912
|
-
if (
|
|
5396
|
+
if (importedHookRecommendation(summary, call, localName, owner, declaration, context, compatibleComponentProps) !== "warning")
|
|
4913
5397
|
continue;
|
|
4914
5398
|
const key = `imported:${call.startIndex}:${call.endIndex}:${localName}`;
|
|
4915
5399
|
if (seen.has(key))
|
|
4916
5400
|
continue;
|
|
4917
5401
|
seen.add(key);
|
|
4918
|
-
const hasBindingMode = summary
|
|
4919
|
-
const
|
|
4920
|
-
const
|
|
4921
|
-
const
|
|
5402
|
+
const hasBindingMode = hasExplicitBindingMode(summary);
|
|
5403
|
+
const observedProperty = observedPropertyForCall(summary, call, context);
|
|
5404
|
+
const hostFeedback = summary.sourceKind === "instance-property" && isStrongHostFeedbackProperty(observedProperty);
|
|
5405
|
+
const likelyContinuousProperty = summary.sourceKind === "instance-property" && Boolean(observedProperty && LIKELY_CONTINUOUS_PROPERTIES.has(observedProperty));
|
|
5406
|
+
const message = hostFeedback ? `${path4} mirrors ${observedProperty} through React state even though every analyzed use is presentation-only, so the measurement rerenders this consumer without needing reconciliation.` : likelyContinuousProperty ? `${path4} mirrors continuously mutable ${observedProperty} through React state, and ${localName} only feeds presentation updates that do not need reconciliation.` : `${path4} returns state updated from a high-frequency source, and ${localName} only feeds presentation updates that do not need React reconciliation.`;
|
|
5407
|
+
const help = hasBindingMode ? `Use the hook's Binding-returning mode${summary.bindingModeParameterName ? ` by passing ${summary.bindingWhenTruthy ? "true" : "false"} for ${summary.bindingModeParameterName}` : ""}. Binding:map can keep pure visual derivations outside the render cycle.` : "Prefer a Binding-returning hook API so continuous presentation updates can reach host properties without rerendering the consumer.";
|
|
4922
5408
|
diagnostics.push({
|
|
4923
5409
|
node: callNameNode(call),
|
|
4924
5410
|
message,
|
|
4925
5411
|
help,
|
|
4926
|
-
fixPreview: hasBindingMode ? bindingModeFixPreview(summary, call, context) : undefined
|
|
4927
|
-
});
|
|
4928
|
-
}
|
|
4929
|
-
for (const call of context.findCalls()) {
|
|
4930
|
-
const path4 = context.getCallPath(call) ?? "";
|
|
4931
|
-
const member = path4.split(/[.:]/).at(-1) ?? path4;
|
|
4932
|
-
const bindingAlternative = context.project.bindingApiAlternatives.get(member);
|
|
4933
|
-
if (!bindingAlternative)
|
|
4934
|
-
continue;
|
|
4935
|
-
const declaration = declarationForCall2(call);
|
|
4936
|
-
const localName = declaration ? declarationNames2(declaration)[0] ?? null : null;
|
|
4937
|
-
const owner = context.containingComponent(call) ?? context.nearestFunction(call);
|
|
4938
|
-
if (!declaration || !localName || !owner || !hasAnyRead(localName, owner, declaration, context))
|
|
4939
|
-
continue;
|
|
4940
|
-
if (!stateReadsAreBindingCompatible(localName, owner, declaration, context, compatibleComponentProps))
|
|
4941
|
-
continue;
|
|
4942
|
-
const field = selectorFieldName(call, context);
|
|
4943
|
-
const key = `state-api:${call.startIndex}:${call.endIndex}:${localName}`;
|
|
4944
|
-
if (seen.has(key))
|
|
4945
|
-
continue;
|
|
4946
|
-
seen.add(key);
|
|
4947
|
-
diagnostics.push({
|
|
4948
|
-
node: callNameNode(call),
|
|
4949
|
-
message: `${path4} subscribes ${localName} through React state${field ? ` for ${field}` : ""}, but ${bindingAlternative} is available and every use of the value only updates bindable host properties.`,
|
|
4950
|
-
help: `Use ${bindingAlternative} so the host property can update without rerendering the component.`,
|
|
4951
|
-
fixPreview: callMemberFixPreview(call, bindingAlternative)
|
|
5412
|
+
fixPreview: hasBindingMode && allReadsDirectlyBindingSubstitutable(localName, owner, declaration, context, compatibleComponentProps) ? bindingModeFixPreview(summary, call, context) : undefined
|
|
4952
5413
|
});
|
|
4953
5414
|
}
|
|
4954
5415
|
for (const binding of context.model.stateBindings) {
|
|
4955
5416
|
const owner = binding.owner;
|
|
4956
5417
|
if (!owner || !owner.isComponent && !owner.isHook)
|
|
4957
5418
|
continue;
|
|
4958
|
-
const
|
|
4959
|
-
if (!
|
|
5419
|
+
const update = bindingUpdateEvidence(binding, importedCallbacks, context);
|
|
5420
|
+
if (!update)
|
|
4960
5421
|
continue;
|
|
4961
5422
|
const localCompatible = stateReadsAreBindingCompatible(binding.valueName, owner, binding.declaration, context, compatibleComponentProps);
|
|
4962
5423
|
const hookCompatible = owner.isHook ? hookCallSitesAreBindingCompatible(binding, context, compatibleComponentProps) : false;
|
|
4963
|
-
|
|
5424
|
+
const strongHostFeedback = hasStrongHostFeedbackEvidence(update);
|
|
5425
|
+
const likelyContinuousProperty = hasLikelyContinuousPropertyEvidence(update);
|
|
5426
|
+
const shouldWarn = owner.isHook ? hookCompatible && (update.highFrequency || likelyContinuousProperty) : localCompatible && (update.highFrequency || strongHostFeedback || likelyContinuousProperty);
|
|
5427
|
+
if (!shouldWarn)
|
|
4964
5428
|
continue;
|
|
4965
5429
|
const key = `${binding.call.startIndex}:${binding.call.endIndex}:${binding.valueName}`;
|
|
4966
5430
|
if (seen.has(key))
|
|
4967
5431
|
continue;
|
|
4968
5432
|
seen.add(key);
|
|
4969
5433
|
if (owner.isHook && hookCompatible) {
|
|
5434
|
+
const properties = likelyContinuousProperty ? [...update.hostFeedbackProperties].sort().join(", ") : "";
|
|
5435
|
+
diagnostics.push({
|
|
5436
|
+
node: callNameNode(binding.call),
|
|
5437
|
+
message: likelyContinuousProperty && !update.highFrequency ? `Hook state ${binding.valueName} mirrors continuously mutable ${properties} and is only consumed by presentation properties at analyzed call sites.` : `Hook state ${binding.valueName} is updated from a high-frequency source and is only consumed by presentation properties at analyzed call sites.`,
|
|
5438
|
+
help: "Consider returning a Binding, or adding a Binding-returning variant of this hook, so continuous presentation updates do not rerender every consumer."
|
|
5439
|
+
});
|
|
5440
|
+
} else if (strongHostFeedback && !update.highFrequency) {
|
|
5441
|
+
const properties = [...update.hostFeedbackProperties].sort().join(", ");
|
|
5442
|
+
diagnostics.push({
|
|
5443
|
+
node: callNameNode(binding.call),
|
|
5444
|
+
message: `State ${binding.valueName} mirrors ${properties} and only feeds presentation updates, so React reconciliation is acting as an unnecessary host-property feedback step.`,
|
|
5445
|
+
help: "Prefer React.useBinding for this host feedback path. Use Binding:map for pure visual derivations and keep React state only when a change needs to affect component structure or React effects.",
|
|
5446
|
+
fixPreview: allReadsDirectlyBindingSubstitutable(binding.valueName, owner, binding.declaration, context, compatibleComponentProps) ? useBindingFixPreview(binding.call) : undefined
|
|
5447
|
+
});
|
|
5448
|
+
} else if (likelyContinuousProperty && !update.highFrequency) {
|
|
5449
|
+
const properties = [...update.hostFeedbackProperties].sort().join(", ");
|
|
4970
5450
|
diagnostics.push({
|
|
4971
5451
|
node: callNameNode(binding.call),
|
|
4972
|
-
message: `
|
|
4973
|
-
help: "
|
|
5452
|
+
message: `State ${binding.valueName} mirrors continuously mutable ${properties} and only feeds presentation updates that do not need React reconciliation.`,
|
|
5453
|
+
help: "Prefer React.useBinding so external visual property changes can reach presentation without routing every update through component state.",
|
|
5454
|
+
fixPreview: allReadsDirectlyBindingSubstitutable(binding.valueName, owner, binding.declaration, context, compatibleComponentProps) ? useBindingFixPreview(binding.call) : undefined
|
|
4974
5455
|
});
|
|
4975
5456
|
} else {
|
|
4976
5457
|
diagnostics.push({
|
|
4977
5458
|
node: callNameNode(binding.call),
|
|
4978
|
-
message: `State ${binding.valueName} is updated
|
|
4979
|
-
help: "Prefer React.useBinding
|
|
4980
|
-
fixPreview: useBindingFixPreview(binding.call)
|
|
5459
|
+
message: `State ${binding.valueName} is updated from a high-frequency callback and only feeds presentation updates that do not need React reconciliation.`,
|
|
5460
|
+
help: "Prefer React.useBinding so continuous visual updates can reach host properties without rerendering the component.",
|
|
5461
|
+
fixPreview: allReadsDirectlyBindingSubstitutable(binding.valueName, owner, binding.declaration, context, compatibleComponentProps) ? useBindingFixPreview(binding.call) : undefined
|
|
4981
5462
|
});
|
|
4982
5463
|
}
|
|
4983
5464
|
}
|
|
@@ -4998,27 +5479,49 @@ var preferBindingOverStateCandidate = {
|
|
|
4998
5479
|
for (const call of context.findCalls()) {
|
|
4999
5480
|
const path4 = context.getCallPath(call) ?? "";
|
|
5000
5481
|
const summary = importedHooks.get(path4);
|
|
5001
|
-
if (!summary
|
|
5482
|
+
if (!summary)
|
|
5002
5483
|
continue;
|
|
5003
5484
|
const declaration = declarationForCall2(call);
|
|
5004
5485
|
const localName = declaration ? declarationNames2(declaration)[0] ?? null : null;
|
|
5005
5486
|
const owner = context.containingComponent(call) ?? context.nearestFunction(call);
|
|
5006
|
-
if (!declaration || !localName || !owner
|
|
5487
|
+
if (!declaration || !localName || !owner)
|
|
5007
5488
|
continue;
|
|
5008
|
-
if (
|
|
5489
|
+
if (importedHookRecommendation(summary, call, localName, owner, declaration, context, compatibleComponentProps) !== "candidate")
|
|
5009
5490
|
continue;
|
|
5010
5491
|
const usage = bindingUsageSummary(localName, owner, declaration, context, compatibleComponentProps);
|
|
5011
|
-
const
|
|
5012
|
-
|
|
5013
|
-
|
|
5492
|
+
const presentationOnly = isPresentationOnly(usage);
|
|
5493
|
+
const observedProperty = observedPropertyForCall(summary, call, context);
|
|
5494
|
+
const strongHostFeedback = summary.sourceKind === "instance-property" && isStrongHostFeedbackProperty(observedProperty);
|
|
5495
|
+
const hasBindingMode = hasExplicitBindingMode(summary);
|
|
5496
|
+
const mirrorsExternalValue = summary.mirrorConfidence !== "none";
|
|
5497
|
+
const visualEffectBridge = strongHostFeedback && usage.effect > 0 && effectOnlyDerivesPresentation(localName, owner, declaration, context, compatibleComponentProps);
|
|
5014
5498
|
const key = `imported:${call.startIndex}:${call.endIndex}:${localName}`;
|
|
5015
5499
|
if (seen.has(key))
|
|
5016
5500
|
continue;
|
|
5017
5501
|
seen.add(key);
|
|
5502
|
+
let message;
|
|
5503
|
+
let help;
|
|
5504
|
+
if (presentationOnly && hasBindingMode) {
|
|
5505
|
+
message = `${path4} has a Binding-returning mode and ${localName} is only used for presentation, but Doctor cannot prove the updates are frequent enough to make the Binding version clearly preferable.`;
|
|
5506
|
+
help = "Consider the Binding mode when this value changes often enough that avoiding reconciliation matters; ordinary React state may be clearer for infrequent semantic updates.";
|
|
5507
|
+
} else if (presentationOnly && strongHostFeedback) {
|
|
5508
|
+
message = `${path4} mirrors ${observedProperty} through React state only for presentation, but the hook does not expose a proven low-friction Binding mode.`;
|
|
5509
|
+
help = "Consider a Binding-returning variant if this host feedback path is performance-sensitive. Keep state when the simpler API is more valuable than avoiding occasional rerenders.";
|
|
5510
|
+
} else if (presentationOnly && mirrorsExternalValue) {
|
|
5511
|
+
message = `${path4} mirrors an external reactive value through React state, and ${localName} is only used for presentation.`;
|
|
5512
|
+
help = "Consider a Binding-returning hook API if this source changes often enough that rerendering is unnecessary. Doctor cannot prove the source's update pressure, so ordinary state may still be the clearer choice.";
|
|
5513
|
+
} else if (visualEffectBridge) {
|
|
5514
|
+
message = `${path4} mirrors ${observedProperty} through React state, and the dependent React effect only derives presentation state or Bindings from ${localName}.`;
|
|
5515
|
+
help = hasBindingMode ? "Consider the hook's Binding mode and move the visual derivation into Binding:map/joinBindings so the measurement does not need an intermediate React rerender." : "Consider a Binding-returning hook variant if this visual feedback path is performance-sensitive; the current effect appears to use React state only as an intermediate presentation step.";
|
|
5516
|
+
} else {
|
|
5517
|
+
message = `${path4} updates ${localName} through React state and some consumers are presentation-only, but other consumers may still require React reconciliation.`;
|
|
5518
|
+
help = "Inspect whether the presentation branch can move to a Binding while structural or effect-driven consumers keep React state. Avoid replacing the value wholesale when React still needs to react to it.";
|
|
5519
|
+
}
|
|
5018
5520
|
diagnostics.push({
|
|
5019
5521
|
node: callNameNode(call),
|
|
5020
|
-
message
|
|
5021
|
-
help
|
|
5522
|
+
message,
|
|
5523
|
+
help,
|
|
5524
|
+
fixPreview: presentationOnly && hasBindingMode && allReadsDirectlyBindingSubstitutable(localName, owner, declaration, context, compatibleComponentProps) ? bindingModeFixPreview(summary, call, context) : undefined
|
|
5022
5525
|
});
|
|
5023
5526
|
}
|
|
5024
5527
|
for (const call of context.findCalls()) {
|
|
@@ -5032,39 +5535,80 @@ var preferBindingOverStateCandidate = {
|
|
|
5032
5535
|
const owner = context.containingComponent(call) ?? context.nearestFunction(call);
|
|
5033
5536
|
if (!declaration || !localName || !owner || !hasAnyRead(localName, owner, declaration, context))
|
|
5034
5537
|
continue;
|
|
5035
|
-
|
|
5538
|
+
const usage = bindingUsageSummary(localName, owner, declaration, context, compatibleComponentProps);
|
|
5539
|
+
const presentationOnly = isPresentationOnly(usage);
|
|
5540
|
+
if (!presentationOnly)
|
|
5036
5541
|
continue;
|
|
5542
|
+
const field = selectorFieldName(call, context);
|
|
5037
5543
|
const key = `state-api:${call.startIndex}:${call.endIndex}:${localName}`;
|
|
5038
5544
|
if (seen.has(key))
|
|
5039
5545
|
continue;
|
|
5040
5546
|
seen.add(key);
|
|
5041
5547
|
diagnostics.push({
|
|
5042
5548
|
node: callNameNode(call),
|
|
5043
|
-
message: `${path4}
|
|
5044
|
-
help: `
|
|
5045
|
-
fixPreview: callMemberFixPreview(call, bindingAlternative)
|
|
5549
|
+
message: `${path4} subscribes ${localName} through React state${field ? ` for ${field}` : ""}, while ${bindingAlternative} is also available and every analyzed consumer is presentation-only.`,
|
|
5550
|
+
help: `Consider ${bindingAlternative} when this value updates frequently enough that avoiding reconciliation matters; state remains reasonable for ordinary semantic store updates.`,
|
|
5551
|
+
fixPreview: allReadsDirectlyBindingSubstitutable(localName, owner, declaration, context, compatibleComponentProps) ? callMemberFixPreview(call, bindingAlternative) : undefined
|
|
5046
5552
|
});
|
|
5047
5553
|
}
|
|
5048
5554
|
for (const binding of context.model.stateBindings) {
|
|
5049
5555
|
const owner = binding.owner;
|
|
5050
|
-
if (!owner || !owner.isComponent || !owner.body)
|
|
5556
|
+
if (!owner || !owner.isComponent && !owner.isHook || !owner.body)
|
|
5051
5557
|
continue;
|
|
5052
|
-
const
|
|
5053
|
-
if (!
|
|
5558
|
+
const update = bindingUpdateEvidence(binding, importedCallbacks, context);
|
|
5559
|
+
if (!update)
|
|
5054
5560
|
continue;
|
|
5055
|
-
|
|
5561
|
+
const localCompatible = stateReadsAreBindingCompatible(binding.valueName, owner, binding.declaration, context, compatibleComponentProps);
|
|
5562
|
+
const hookCompatible = owner.isHook ? hookCallSitesAreBindingCompatible(binding, context, compatibleComponentProps) : false;
|
|
5563
|
+
const strongHostFeedback = hasStrongHostFeedbackEvidence(update);
|
|
5564
|
+
const likelyContinuousProperty = hasLikelyContinuousPropertyEvidence(update);
|
|
5565
|
+
const mirrorsExternalValue = update.mirrorConfidence !== "none";
|
|
5566
|
+
if (owner.isHook) {
|
|
5567
|
+
if (hookCompatible && (update.highFrequency || likelyContinuousProperty))
|
|
5568
|
+
continue;
|
|
5569
|
+
if (!(hookCompatible && (strongHostFeedback || mirrorsExternalValue)))
|
|
5570
|
+
continue;
|
|
5571
|
+
const key2 = `hook-state:${binding.call.startIndex}:${binding.call.endIndex}:${binding.valueName}`;
|
|
5572
|
+
if (seen.has(key2))
|
|
5573
|
+
continue;
|
|
5574
|
+
seen.add(key2);
|
|
5575
|
+
const measurement = strongHostFeedback ? "a presentation measurement" : "an external reactive value";
|
|
5576
|
+
diagnostics.push({
|
|
5577
|
+
node: callNameNode(binding.call),
|
|
5578
|
+
message: `Hook state ${binding.valueName} mirrors ${measurement} and is only consumed by presentation properties at analyzed call sites, but converting it requires changing the hook API.`,
|
|
5579
|
+
help: "Consider adding a Binding-returning variant when this reactive path updates often enough that avoiding reconciliation matters; keeping the state API is reasonable when ordinary React state is simpler."
|
|
5580
|
+
});
|
|
5056
5581
|
continue;
|
|
5582
|
+
}
|
|
5057
5583
|
const usage = bindingUsageSummary(binding.valueName, owner, binding.declaration, context, compatibleComponentProps);
|
|
5058
5584
|
if (!hasCandidatePresentationEvidence(usage))
|
|
5059
5585
|
continue;
|
|
5586
|
+
if (localCompatible && (update.highFrequency || strongHostFeedback || likelyContinuousProperty))
|
|
5587
|
+
continue;
|
|
5588
|
+
if (localCompatible && mirrorsExternalValue) {
|
|
5589
|
+
const key2 = `state:${binding.call.startIndex}:${binding.call.endIndex}:${binding.valueName}`;
|
|
5590
|
+
if (seen.has(key2))
|
|
5591
|
+
continue;
|
|
5592
|
+
seen.add(key2);
|
|
5593
|
+
diagnostics.push({
|
|
5594
|
+
node: callNameNode(binding.call),
|
|
5595
|
+
message: `State ${binding.valueName} mirrors an external reactive value and only feeds presentation updates, but Doctor cannot prove the source changes frequently enough for Binding to be clearly preferable.`,
|
|
5596
|
+
help: "Consider React.useBinding when this source is a stream of presentation values rather than semantic React state. Keep useState when the external event represents an infrequent UI state transition."
|
|
5597
|
+
});
|
|
5598
|
+
continue;
|
|
5599
|
+
}
|
|
5600
|
+
const mixedHighFrequency = update.highFrequency || likelyContinuousProperty;
|
|
5601
|
+
const mixedHostFeedback = strongHostFeedback && usage.structural > 0 && usage.effect === 0;
|
|
5602
|
+
if (!mixedHighFrequency && !mixedHostFeedback)
|
|
5603
|
+
continue;
|
|
5060
5604
|
const key = `state:${binding.call.startIndex}:${binding.call.endIndex}:${binding.valueName}`;
|
|
5061
5605
|
if (seen.has(key))
|
|
5062
5606
|
continue;
|
|
5063
5607
|
seen.add(key);
|
|
5064
5608
|
diagnostics.push({
|
|
5065
5609
|
node: callNameNode(binding.call),
|
|
5066
|
-
message: `State ${binding.valueName}
|
|
5067
|
-
help: "Inspect whether
|
|
5610
|
+
message: `State ${binding.valueName} has presentation-oriented consumers, but other uses still appear to need React reconciliation.`,
|
|
5611
|
+
help: "Inspect whether the presentation branch can move to React.useBinding while structural consumers keep a smaller semantic state value. Splitting the responsibilities can remove hot-path rerenders without forcing Binding semantics onto the whole value."
|
|
5068
5612
|
});
|
|
5069
5613
|
}
|
|
5070
5614
|
return diagnostics;
|
|
@@ -6423,6 +6967,16 @@ function nodeLocation(node) {
|
|
|
6423
6967
|
endColumn: node.endPosition.column + 1
|
|
6424
6968
|
};
|
|
6425
6969
|
}
|
|
6970
|
+
function sameLocation(left, right) {
|
|
6971
|
+
return left.line === right.line && left.column === right.column && left.endLine === right.endLine && left.endColumn === right.endColumn;
|
|
6972
|
+
}
|
|
6973
|
+
function removeSupersededDiagnostics(diagnostics) {
|
|
6974
|
+
return diagnostics.filter((diagnostic) => {
|
|
6975
|
+
if (diagnostic.rule !== "react-luau/rerender-high-frequency-state")
|
|
6976
|
+
return true;
|
|
6977
|
+
return !diagnostics.some((candidate) => candidate.rule === "react-luau/prefer-binding-over-state" && sameLocation(candidate.location, diagnostic.location) && SEVERITY_RANK[candidate.severity] >= SEVERITY_RANK[diagnostic.severity]);
|
|
6978
|
+
});
|
|
6979
|
+
}
|
|
6426
6980
|
function toDiagnostic(file, ruleId, category, severity, input) {
|
|
6427
6981
|
const node = input.node;
|
|
6428
6982
|
const highlights = input.highlights?.map(nodeLocation);
|
|
@@ -6480,11 +7034,12 @@ async function analyzeReactFile(input, tree) {
|
|
|
6480
7034
|
if (rule.id === "react-luau/parse-error" && findings.length > 0)
|
|
6481
7035
|
break;
|
|
6482
7036
|
}
|
|
7037
|
+
const filteredDiagnostics = removeSupersededDiagnostics(diagnostics);
|
|
6483
7038
|
return {
|
|
6484
7039
|
relativePath: input.relativePath,
|
|
6485
7040
|
isReactFile: model.isReactFile,
|
|
6486
7041
|
scanned: true,
|
|
6487
|
-
diagnostics
|
|
7042
|
+
diagnostics: filteredDiagnostics
|
|
6488
7043
|
};
|
|
6489
7044
|
}
|
|
6490
7045
|
|
|
@@ -7455,5 +8010,5 @@ parentPort.on("message", async (request) => {
|
|
|
7455
8010
|
}
|
|
7456
8011
|
});
|
|
7457
8012
|
|
|
7458
|
-
//# debugId=
|
|
8013
|
+
//# debugId=07C5601A473D8F7164756E2164756E21
|
|
7459
8014
|
//# sourceMappingURL=scan-worker.js.map
|