@colrealpro/react-luau-doctor 0.20.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/cli.js +511 -142
- package/dist/cli.js.map +10 -8
- package/dist/config.schema.json +3 -3
- package/dist/scan-worker.js +510 -141
- package/dist/scan-worker.js.map +10 -8
- package/package.json +1 -1
package/dist/scan-worker.js
CHANGED
|
@@ -737,6 +737,12 @@ import path2 from "path";
|
|
|
737
737
|
// src/ast/local-values.ts
|
|
738
738
|
var valueCache = new WeakMap;
|
|
739
739
|
var apiCache = new WeakMap;
|
|
740
|
+
function isUnboundIdentifier(context, node) {
|
|
741
|
+
return node.type === "identifier" && visibleValue(context, node) === undefined;
|
|
742
|
+
}
|
|
743
|
+
function localValueBinding(context, node) {
|
|
744
|
+
return node.type === "identifier" ? visibleValue(context, node) : null;
|
|
745
|
+
}
|
|
740
746
|
function unwrapExpression(node) {
|
|
741
747
|
while (["parenthesized_expression", "cast_expression"].includes(node.type) && node.namedChildren[0]) {
|
|
742
748
|
node = node.namedChildren[0];
|
|
@@ -2980,6 +2986,242 @@ var effectNeedsCleanup = {
|
|
|
2980
2986
|
}
|
|
2981
2987
|
};
|
|
2982
2988
|
|
|
2989
|
+
// src/api-reference.ts
|
|
2990
|
+
var functions = new Map;
|
|
2991
|
+
function register(namespace, members, reference) {
|
|
2992
|
+
for (const member of members)
|
|
2993
|
+
functions.set(`${namespace}.${member}`, reference);
|
|
2994
|
+
}
|
|
2995
|
+
for (const type of [
|
|
2996
|
+
"Instance",
|
|
2997
|
+
"Random",
|
|
2998
|
+
"RaycastParams",
|
|
2999
|
+
"OverlapParams",
|
|
3000
|
+
"CatalogSearchParams",
|
|
3001
|
+
"SharedTable"
|
|
3002
|
+
]) {
|
|
3003
|
+
register(type, ["new"], {
|
|
3004
|
+
returnType: type,
|
|
3005
|
+
allocation: "new",
|
|
3006
|
+
equality: "reference"
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
register("table", ["clone", "create", "pack"], {
|
|
3010
|
+
returnType: "table",
|
|
3011
|
+
allocation: "new",
|
|
3012
|
+
equality: "reference"
|
|
3013
|
+
});
|
|
3014
|
+
register("SharedTable", ["clone", "cloneAndFreeze"], {
|
|
3015
|
+
returnType: "SharedTable",
|
|
3016
|
+
allocation: "new",
|
|
3017
|
+
equality: "reference"
|
|
3018
|
+
});
|
|
3019
|
+
register("string", ["split"], {
|
|
3020
|
+
returnType: "table",
|
|
3021
|
+
allocation: "new",
|
|
3022
|
+
equality: "reference"
|
|
3023
|
+
});
|
|
3024
|
+
register("buffer", ["create", "fromstring"], {
|
|
3025
|
+
returnType: "buffer",
|
|
3026
|
+
allocation: "new",
|
|
3027
|
+
equality: "reference"
|
|
3028
|
+
});
|
|
3029
|
+
register("coroutine", ["create"], {
|
|
3030
|
+
returnType: "thread",
|
|
3031
|
+
allocation: "new",
|
|
3032
|
+
equality: "reference"
|
|
3033
|
+
});
|
|
3034
|
+
register("coroutine", ["wrap"], {
|
|
3035
|
+
returnType: "function",
|
|
3036
|
+
allocation: "new",
|
|
3037
|
+
equality: "reference"
|
|
3038
|
+
});
|
|
3039
|
+
register("table", ["freeze"], {
|
|
3040
|
+
returnType: "table",
|
|
3041
|
+
allocation: "argument",
|
|
3042
|
+
equality: "reference",
|
|
3043
|
+
argumentIndex: 0
|
|
3044
|
+
});
|
|
3045
|
+
for (const [type, members] of Object.entries({
|
|
3046
|
+
Color3: ["new", "fromRGB", "fromHSV", "fromHex"],
|
|
3047
|
+
ColorSequence: ["new"],
|
|
3048
|
+
ColorSequenceKeypoint: ["new"],
|
|
3049
|
+
NumberSequence: ["new"],
|
|
3050
|
+
NumberSequenceKeypoint: ["new"],
|
|
3051
|
+
NumberRange: ["new"],
|
|
3052
|
+
UDim: ["new"],
|
|
3053
|
+
UDim2: ["new", "fromScale", "fromOffset"],
|
|
3054
|
+
Vector2: ["new"],
|
|
3055
|
+
Vector3: ["new", "fromNormalId", "fromAxis"],
|
|
3056
|
+
CFrame: [
|
|
3057
|
+
"new",
|
|
3058
|
+
"Angles",
|
|
3059
|
+
"fromEulerAnglesXYZ",
|
|
3060
|
+
"fromEulerAnglesYXZ",
|
|
3061
|
+
"fromOrientation",
|
|
3062
|
+
"fromAxisAngle",
|
|
3063
|
+
"fromMatrix",
|
|
3064
|
+
"lookAt",
|
|
3065
|
+
"lookAlong"
|
|
3066
|
+
],
|
|
3067
|
+
Rect: ["new"],
|
|
3068
|
+
BrickColor: ["new", "palette"]
|
|
3069
|
+
})) {
|
|
3070
|
+
register(type, members, {
|
|
3071
|
+
returnType: type,
|
|
3072
|
+
allocation: "new",
|
|
3073
|
+
equality: "value"
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
var instanceMethods = new Map([
|
|
3077
|
+
...["GetChildren", "GetDescendants", "GetAttributes", "GetTags"].map((member) => [
|
|
3078
|
+
member,
|
|
3079
|
+
{ returnType: "table", allocation: "new", equality: "reference" }
|
|
3080
|
+
]),
|
|
3081
|
+
[
|
|
3082
|
+
"Clone",
|
|
3083
|
+
{ returnType: "Instance", allocation: "new", equality: "reference" }
|
|
3084
|
+
]
|
|
3085
|
+
]);
|
|
3086
|
+
function apiFunctionReference(path) {
|
|
3087
|
+
return functions.get(path) ?? null;
|
|
3088
|
+
}
|
|
3089
|
+
function globalPath(context, expression, seen = new Set) {
|
|
3090
|
+
const node = unwrapExpression(expression);
|
|
3091
|
+
if (seen.has(node.id))
|
|
3092
|
+
return null;
|
|
3093
|
+
seen.add(node.id);
|
|
3094
|
+
if (node.type === "identifier") {
|
|
3095
|
+
if (isUnboundIdentifier(context, node))
|
|
3096
|
+
return node.text;
|
|
3097
|
+
const binding = localValueBinding(context, node);
|
|
3098
|
+
return binding?.returnIndex === 0 ? globalPath(context, binding.value, seen) : null;
|
|
3099
|
+
}
|
|
3100
|
+
if (node.type !== "dot_index_expression")
|
|
3101
|
+
return null;
|
|
3102
|
+
const namespace = node.childForFieldName("table");
|
|
3103
|
+
const member = node.childForFieldName("field");
|
|
3104
|
+
const path = namespace && globalPath(context, namespace, seen);
|
|
3105
|
+
return path && member ? `${path}.${member.text}` : null;
|
|
3106
|
+
}
|
|
3107
|
+
function isInstanceReceiver(context, expression, seen = new Set) {
|
|
3108
|
+
const node = unwrapExpression(expression);
|
|
3109
|
+
if (seen.has(node.id))
|
|
3110
|
+
return false;
|
|
3111
|
+
seen.add(node.id);
|
|
3112
|
+
if (node.type === "identifier") {
|
|
3113
|
+
if (isUnboundIdentifier(context, node))
|
|
3114
|
+
return ["game", "workspace", "script"].includes(node.text);
|
|
3115
|
+
const binding = localValueBinding(context, node);
|
|
3116
|
+
return binding?.returnIndex === 0 && isInstanceReceiver(context, binding.value, seen);
|
|
3117
|
+
}
|
|
3118
|
+
if (node.type === "dot_index_expression") {
|
|
3119
|
+
const receiver = node.childForFieldName("table");
|
|
3120
|
+
return Boolean(receiver && isInstanceReceiver(context, receiver, seen));
|
|
3121
|
+
}
|
|
3122
|
+
if (node.type !== "function_call")
|
|
3123
|
+
return false;
|
|
3124
|
+
const name = node.childForFieldName("name");
|
|
3125
|
+
if (!name)
|
|
3126
|
+
return false;
|
|
3127
|
+
if (globalPath(context, name) === "Instance.new")
|
|
3128
|
+
return true;
|
|
3129
|
+
if (name.type !== "method_index_expression")
|
|
3130
|
+
return false;
|
|
3131
|
+
const receiver = name.childForFieldName("table");
|
|
3132
|
+
const member = name.childForFieldName("method")?.text;
|
|
3133
|
+
return Boolean(receiver && member && [
|
|
3134
|
+
"GetService",
|
|
3135
|
+
"WaitForChild",
|
|
3136
|
+
"FindFirstChild",
|
|
3137
|
+
"FindFirstAncestor",
|
|
3138
|
+
"Clone"
|
|
3139
|
+
].includes(member) && isInstanceReceiver(context, receiver, seen));
|
|
3140
|
+
}
|
|
3141
|
+
function apiReferenceForCall(context, call) {
|
|
3142
|
+
const name = call.childForFieldName("name");
|
|
3143
|
+
if (!name)
|
|
3144
|
+
return null;
|
|
3145
|
+
const path = globalPath(context, name);
|
|
3146
|
+
if (path)
|
|
3147
|
+
return apiFunctionReference(path);
|
|
3148
|
+
if (name.type !== "method_index_expression")
|
|
3149
|
+
return null;
|
|
3150
|
+
const receiver = name.childForFieldName("table");
|
|
3151
|
+
const member = name.childForFieldName("method")?.text;
|
|
3152
|
+
return receiver && member && isInstanceReceiver(context, receiver) ? instanceMethods.get(member) ?? null : null;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
// src/ast/value-identity.ts
|
|
3156
|
+
function freshValueKind(context, expression, lifetime, seen = new Set, returnIndex = 0, lifetimes = new Set([lifetime])) {
|
|
3157
|
+
if (!expression)
|
|
3158
|
+
return null;
|
|
3159
|
+
const node = unwrapExpression(expression);
|
|
3160
|
+
if (seen.has(node.id) || seen.size >= 64)
|
|
3161
|
+
return null;
|
|
3162
|
+
const visited = new Set(seen).add(node.id);
|
|
3163
|
+
const analyze = (value, owner = lifetime, index = 0) => freshValueKind(context, value, owner, visited, index, new Set(lifetimes).add(owner));
|
|
3164
|
+
if (node.type === "identifier") {
|
|
3165
|
+
const binding = resolveLocalValue(context, node);
|
|
3166
|
+
const owner = binding && context.nearestFunction(binding.value);
|
|
3167
|
+
if (!binding || !owner || !lifetimes.has(owner))
|
|
3168
|
+
return null;
|
|
3169
|
+
return analyze(binding.value, lifetime, binding.returnIndex);
|
|
3170
|
+
}
|
|
3171
|
+
if (returnIndex === 0) {
|
|
3172
|
+
if (node.type === "table_constructor")
|
|
3173
|
+
return "table";
|
|
3174
|
+
if (node.type === "function_definition" || node.type === "function_declaration")
|
|
3175
|
+
return "function";
|
|
3176
|
+
if (node.type === "binary_expression") {
|
|
3177
|
+
const operator = node.children.find((child) => ["and", "or"].includes(child.type))?.type;
|
|
3178
|
+
if (operator === "and")
|
|
3179
|
+
return analyze(node.childForFieldName("right"));
|
|
3180
|
+
if (operator === "or")
|
|
3181
|
+
return analyze(node.childForFieldName("left")) ?? analyze(node.childForFieldName("right"));
|
|
3182
|
+
}
|
|
3183
|
+
if (node.type === "if_expression") {
|
|
3184
|
+
for (const branch of [
|
|
3185
|
+
node,
|
|
3186
|
+
...node.namedChildren.filter((child) => ["elseif_clause", "else_clause"].includes(child.type))
|
|
3187
|
+
]) {
|
|
3188
|
+
const kind = analyze(branch.childForFieldName("consequence"));
|
|
3189
|
+
if (kind)
|
|
3190
|
+
return kind;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
if (node.type !== "function_call")
|
|
3195
|
+
return null;
|
|
3196
|
+
const api = apiReferenceForCall(context, node);
|
|
3197
|
+
if (api && returnIndex === 0) {
|
|
3198
|
+
if (api.allocation === "argument")
|
|
3199
|
+
return analyze(context.callArguments(node)[api.argumentIndex ?? 0] ?? null);
|
|
3200
|
+
if (api.equality !== "reference")
|
|
3201
|
+
return null;
|
|
3202
|
+
return api.returnType === "table" || api.returnType === "function" ? api.returnType : "object";
|
|
3203
|
+
}
|
|
3204
|
+
const name = node.childForFieldName("name");
|
|
3205
|
+
const factory = name && resolveLocalFunction(context, name);
|
|
3206
|
+
if (!factory?.body)
|
|
3207
|
+
return null;
|
|
3208
|
+
for (const statement of context.walk(factory.body)) {
|
|
3209
|
+
if (statement.type !== "return_statement" || context.nearestFunction(statement) !== factory)
|
|
3210
|
+
continue;
|
|
3211
|
+
const values = statement.namedChildren.find((child) => child.type === "expression_list")?.namedChildren ?? [];
|
|
3212
|
+
const value = values[returnIndex] ?? values.at(-1);
|
|
3213
|
+
if (!value)
|
|
3214
|
+
continue;
|
|
3215
|
+
const index = Math.max(0, returnIndex - values.length + 1);
|
|
3216
|
+
if (index > 0 && value.type !== "function_call")
|
|
3217
|
+
continue;
|
|
3218
|
+
const kind = analyze(value, factory, index);
|
|
3219
|
+
if (kind)
|
|
3220
|
+
return kind;
|
|
3221
|
+
}
|
|
3222
|
+
return null;
|
|
3223
|
+
}
|
|
3224
|
+
|
|
2983
3225
|
// src/rules/effect-state-rules.ts
|
|
2984
3226
|
var HOOKS_WITH_DEPS = new Set([
|
|
2985
3227
|
"React.useEffect",
|
|
@@ -3043,23 +3285,6 @@ function hasExternalSubscription(node, context) {
|
|
|
3043
3285
|
}
|
|
3044
3286
|
return false;
|
|
3045
3287
|
}
|
|
3046
|
-
function directLocalInitializer(owner, name) {
|
|
3047
|
-
if (!owner.body)
|
|
3048
|
-
return null;
|
|
3049
|
-
for (const statement of owner.body.namedChildren) {
|
|
3050
|
-
if (statement.type !== "variable_declaration")
|
|
3051
|
-
continue;
|
|
3052
|
-
const assignment = statement.namedChildren.find((child) => child.type === "assignment_statement");
|
|
3053
|
-
const variableList = assignment?.namedChildren.find((child) => child.type === "variable_list");
|
|
3054
|
-
const expressionList = assignment?.namedChildren.find((child) => child.type === "expression_list");
|
|
3055
|
-
const names = variableList?.namedChildren.filter((child) => child.type === "identifier") ?? [];
|
|
3056
|
-
const expressions = expressionList?.namedChildren ?? [];
|
|
3057
|
-
const index = names.findIndex((child) => child.text === name);
|
|
3058
|
-
if (index >= 0)
|
|
3059
|
-
return expressions[index] ?? expressions[0] ?? null;
|
|
3060
|
-
}
|
|
3061
|
-
return null;
|
|
3062
|
-
}
|
|
3063
3288
|
function setterCalledOutsideCallback(context, owner, callback, setterName) {
|
|
3064
3289
|
if (!owner.body)
|
|
3065
3290
|
return false;
|
|
@@ -3099,20 +3324,6 @@ function guaranteedRepeatedStateChange(argument, stateName, context) {
|
|
|
3099
3324
|
}
|
|
3100
3325
|
return false;
|
|
3101
3326
|
}
|
|
3102
|
-
function freshDependencyKind(node, owner) {
|
|
3103
|
-
if (node.type === "table_constructor")
|
|
3104
|
-
return "table";
|
|
3105
|
-
if (node.type === "function_definition")
|
|
3106
|
-
return "function";
|
|
3107
|
-
if (node.type === "identifier") {
|
|
3108
|
-
const initializer = directLocalInitializer(owner, node.text);
|
|
3109
|
-
if (initializer?.type === "table_constructor")
|
|
3110
|
-
return "table";
|
|
3111
|
-
if (initializer?.type === "function_definition")
|
|
3112
|
-
return "function";
|
|
3113
|
-
}
|
|
3114
|
-
return null;
|
|
3115
|
-
}
|
|
3116
3327
|
var noDerivedStateEffect = {
|
|
3117
3328
|
id: "react-luau/no-derived-state-effect",
|
|
3118
3329
|
category: "Effects",
|
|
@@ -3224,7 +3435,7 @@ var noEffectWithFreshDeps = {
|
|
|
3224
3435
|
id: "react-luau/no-effect-with-fresh-deps",
|
|
3225
3436
|
category: "Hooks",
|
|
3226
3437
|
severity: "error",
|
|
3227
|
-
description: "Dependency tables should not contain
|
|
3438
|
+
description: "Dependency tables should not contain reference values recreated during render",
|
|
3228
3439
|
run(context) {
|
|
3229
3440
|
const diagnostics = [];
|
|
3230
3441
|
for (const call of context.findCalls()) {
|
|
@@ -3236,14 +3447,14 @@ var noEffectWithFreshDeps = {
|
|
|
3236
3447
|
if (!owner || !parts)
|
|
3237
3448
|
continue;
|
|
3238
3449
|
for (const dependency of dependencyExpressions(parts.deps)) {
|
|
3239
|
-
const kind =
|
|
3450
|
+
const kind = freshValueKind(context, dependency, owner);
|
|
3240
3451
|
if (!kind)
|
|
3241
3452
|
continue;
|
|
3242
3453
|
const label = dependency.type === "identifier" ? ` ${dependency.text}` : "";
|
|
3243
3454
|
diagnostics.push({
|
|
3244
3455
|
node: dependency,
|
|
3245
|
-
message: `${path} depends on${label || " an inline value"} that
|
|
3246
|
-
help: "Move the value inside the hook callback and depend on its simple inputs, or intentionally stabilize the value with useMemo/useCallback when identity is part of the contract
|
|
3456
|
+
message: `${path} depends on${label || " an inline value"} that can be a new ${kind} when the component renders`,
|
|
3457
|
+
help: "Move the value inside the hook callback and depend on its simple inputs, or intentionally stabilize the value with useMemo/useCallback when identity is part of the contract"
|
|
3247
3458
|
});
|
|
3248
3459
|
}
|
|
3249
3460
|
}
|
|
@@ -5350,40 +5561,32 @@ function componentRenderCost(context, owner) {
|
|
|
5350
5561
|
parts.push(`${discovery} Instance discovery call${discovery === 1 ? "" : "s"}`);
|
|
5351
5562
|
return parts;
|
|
5352
5563
|
}
|
|
5353
|
-
function
|
|
5354
|
-
const
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
continue;
|
|
5361
|
-
const localName = match[1];
|
|
5362
|
-
const memoKind = resolveModuleReference(normalizeRequireTarget(match[2]), context.project.memoizedModules);
|
|
5363
|
-
if (memoKind)
|
|
5364
|
-
result.set(localName, memoKind);
|
|
5564
|
+
function memoizedComponentKind(context, expression) {
|
|
5565
|
+
const binding = resolveLocalValue(context, expression);
|
|
5566
|
+
if (!binding || binding.returnIndex !== 0 || binding.value.type !== "function_call")
|
|
5567
|
+
return null;
|
|
5568
|
+
const call = binding.value;
|
|
5569
|
+
if (reactApiPath(context, call) === "React.memo") {
|
|
5570
|
+
return context.callArguments(call).length > 1 ? "custom" : "shallow";
|
|
5365
5571
|
}
|
|
5366
|
-
|
|
5572
|
+
const target = context.callArguments(call)[0];
|
|
5573
|
+
if (context.getCallPath(call) !== "require" || !target)
|
|
5574
|
+
return null;
|
|
5575
|
+
return resolveModuleReference(normalizeRequireTarget(target.text), context.project.memoizedModules) ?? null;
|
|
5367
5576
|
}
|
|
5368
5577
|
function fieldLabel(field) {
|
|
5369
5578
|
return fieldName(field) ?? field.childForFieldName("name")?.text ?? "prop";
|
|
5370
5579
|
}
|
|
5371
|
-
function isInlineFreshValue(node) {
|
|
5372
|
-
return node?.type === "table_constructor" || node?.type === "function_definition";
|
|
5373
|
-
}
|
|
5374
5580
|
var rerenderUnstableMemoProps = {
|
|
5375
5581
|
id: "react-luau/rerender-unstable-memo-props",
|
|
5376
5582
|
category: "Performance",
|
|
5377
5583
|
severity: "warning",
|
|
5378
|
-
description: "Warn when
|
|
5584
|
+
description: "Warn when props created during render can defeat shallow React.memo comparisons",
|
|
5379
5585
|
guidance: {
|
|
5380
|
-
explanation: "
|
|
5381
|
-
help: "Stabilize props that change each render if this child is expensive, move construction into the child, or remove ineffective memoization
|
|
5586
|
+
explanation: "New tables, functions, and reference objects compare unequal even when their contents match",
|
|
5587
|
+
help: "Stabilize props that change each render if this child is expensive, move construction into the child, or remove ineffective memoization"
|
|
5382
5588
|
},
|
|
5383
5589
|
run(context) {
|
|
5384
|
-
const imports = memoizedImports(context);
|
|
5385
|
-
if (imports.size === 0)
|
|
5386
|
-
return [];
|
|
5387
5590
|
const diagnostics = [];
|
|
5388
5591
|
for (const call of context.findCalls()) {
|
|
5389
5592
|
if (context.resolveCallPath(context.getCallPath(call) ?? "") !== "React.createElement")
|
|
@@ -5393,12 +5596,13 @@ var rerenderUnstableMemoProps = {
|
|
|
5393
5596
|
continue;
|
|
5394
5597
|
const args = context.callArguments(call);
|
|
5395
5598
|
const childName = args[0]?.text.trim() ?? "";
|
|
5396
|
-
if (
|
|
5599
|
+
if (!args[0] || memoizedComponentKind(context, args[0]) !== "shallow")
|
|
5397
5600
|
continue;
|
|
5398
|
-
const
|
|
5399
|
-
|
|
5601
|
+
const propsBinding = args[1] && resolveLocalValue(context, args[1]);
|
|
5602
|
+
const props = propsBinding?.returnIndex === 0 ? propsBinding.value : null;
|
|
5603
|
+
if (!props || props.type !== "table_constructor" || context.nearestFunction(props) !== component)
|
|
5400
5604
|
continue;
|
|
5401
|
-
const unstableFields = props.namedChildren.filter((node) => node.type === "field").filter((field) =>
|
|
5605
|
+
const unstableFields = props.namedChildren.filter((node) => node.type === "field").filter((field) => freshValueKind(context, fieldValue(field), component));
|
|
5402
5606
|
if (unstableFields.length === 0)
|
|
5403
5607
|
continue;
|
|
5404
5608
|
const unstable = unstableFields.map(fieldLabel);
|
|
@@ -5407,13 +5611,13 @@ var rerenderUnstableMemoProps = {
|
|
|
5407
5611
|
node: highlights[0],
|
|
5408
5612
|
highlights,
|
|
5409
5613
|
severity: "warning",
|
|
5410
|
-
message: `Memoized ${childName}
|
|
5411
|
-
summary: `${childName} gets ${unstable.length}
|
|
5614
|
+
message: `Memoized ${childName} can receive fresh ${unstable.join(", ")} prop${unstable.length === 1 ? "" : "s"} when its parent renders`,
|
|
5615
|
+
summary: `${childName} gets ${unstable.length} ${unstable.length === 1 ? "prop" : "props"} created during render that can prevent memoization from skipping it`,
|
|
5412
5616
|
editorRanges: highlights.map((node, index) => ({
|
|
5413
5617
|
node,
|
|
5414
|
-
summary: `${childName} gets a
|
|
5618
|
+
summary: `${childName} gets a ${unstable[index]} prop created during render that can prevent memoization from skipping it`
|
|
5415
5619
|
})),
|
|
5416
|
-
help: "Fresh
|
|
5620
|
+
help: "Fresh reference identity can prevent the shallow memo comparison from skipping this child. Stabilize the prop when the parent rerenders frequently, move construction into the child, or remove ineffective memoization"
|
|
5417
5621
|
});
|
|
5418
5622
|
}
|
|
5419
5623
|
return diagnostics;
|
|
@@ -7587,30 +7791,32 @@ function currentConditionalHookMode(context) {
|
|
|
7587
7791
|
}
|
|
7588
7792
|
return null;
|
|
7589
7793
|
}
|
|
7590
|
-
function controlledParameterIndex(controlFlow, fn, summary) {
|
|
7794
|
+
function controlledParameterIndex(controlFlow, fn, summary, node) {
|
|
7591
7795
|
if (controlFlow.type !== "if_statement")
|
|
7592
7796
|
return null;
|
|
7593
|
-
const
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
new RegExp(`^if\\s+${escaped}\\s*$`),
|
|
7598
|
-
new RegExp(`^if\\s+not\\s+${escaped}\\s*$`),
|
|
7599
|
-
new RegExp(`^if\\s+${escaped}\\s*(?:==|~=)\\s*(?:true|false|nil)\\s*$`),
|
|
7600
|
-
new RegExp(`^if\\s+(?:true|false|nil)\\s*(?:==|~=)\\s*${escaped}\\s*$`)
|
|
7601
|
-
];
|
|
7602
|
-
if (patterns.some((pattern) => pattern.test(header)))
|
|
7603
|
-
return index;
|
|
7604
|
-
}
|
|
7797
|
+
const conditions = guardConditions(controlFlow, node);
|
|
7798
|
+
if (conditions.length === 0)
|
|
7799
|
+
return null;
|
|
7800
|
+
const variables = { ...summary.conditionVariables };
|
|
7605
7801
|
for (const index of summary.parameterIndexes) {
|
|
7606
7802
|
const name = fn.parameters[index];
|
|
7607
|
-
if (
|
|
7608
|
-
|
|
7609
|
-
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7610
|
-
if (new RegExp(`^if\\s+(?:not\\s+)?${escaped}\\s*$`).test(header))
|
|
7611
|
-
return index;
|
|
7803
|
+
if (name)
|
|
7804
|
+
variables[name] ??= index;
|
|
7612
7805
|
}
|
|
7613
|
-
|
|
7806
|
+
const indexes = conditions.map((condition) => {
|
|
7807
|
+
for (const [name, index] of Object.entries(variables)) {
|
|
7808
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7809
|
+
const patterns = [
|
|
7810
|
+
new RegExp(`^(?:not\\s+)?${escaped}\\s*$`),
|
|
7811
|
+
new RegExp(`^${escaped}\\s*(?:==|~=)\\s*(?:true|false|nil)\\s*$`),
|
|
7812
|
+
new RegExp(`^(?:true|false|nil)\\s*(?:==|~=)\\s*${escaped}\\s*$`)
|
|
7813
|
+
];
|
|
7814
|
+
if (patterns.some((pattern) => pattern.test(condition.text.trim())))
|
|
7815
|
+
return index;
|
|
7816
|
+
}
|
|
7817
|
+
return null;
|
|
7818
|
+
});
|
|
7819
|
+
return indexes.every((index) => index !== null) ? indexes[0] : null;
|
|
7614
7820
|
}
|
|
7615
7821
|
function dynamicHookModeFixPreview(hookName, argumentText) {
|
|
7616
7822
|
return {
|
|
@@ -7812,28 +8018,104 @@ function staticModuleTables(context) {
|
|
|
7812
8018
|
}
|
|
7813
8019
|
return candidates;
|
|
7814
8020
|
}
|
|
7815
|
-
function
|
|
7816
|
-
|
|
7817
|
-
|
|
8021
|
+
function containsNode(container, node) {
|
|
8022
|
+
return node.startIndex >= container.startIndex && node.endIndex <= container.endIndex;
|
|
8023
|
+
}
|
|
8024
|
+
function branchContaining(controlFlow, node) {
|
|
8025
|
+
return controlFlow.namedChildren.find((child) => containsNode(child, node)) ?? null;
|
|
8026
|
+
}
|
|
8027
|
+
function guardConditions(controlFlow, node) {
|
|
7818
8028
|
const condition = controlFlow.namedChildren[0];
|
|
7819
8029
|
if (!condition)
|
|
7820
|
-
return
|
|
8030
|
+
return [];
|
|
8031
|
+
if (controlFlow.type !== "if_statement" && controlFlow.type !== "if_expression")
|
|
8032
|
+
return [condition];
|
|
8033
|
+
if (containsNode(condition, node))
|
|
8034
|
+
return [];
|
|
8035
|
+
const conditions = [condition];
|
|
8036
|
+
for (const clause of controlFlow.namedChildren.slice(1)) {
|
|
8037
|
+
if (clause.startIndex > node.startIndex)
|
|
8038
|
+
break;
|
|
8039
|
+
if (clause.type !== "elseif_statement" && clause.type !== "elseif_clause")
|
|
8040
|
+
continue;
|
|
8041
|
+
const clauseCondition = clause.namedChildren[0];
|
|
8042
|
+
if (clauseCondition && !containsNode(clauseCondition, node))
|
|
8043
|
+
conditions.push(clauseCondition);
|
|
8044
|
+
}
|
|
8045
|
+
return conditions;
|
|
8046
|
+
}
|
|
8047
|
+
function conditionallyContains(controlFlow, node) {
|
|
8048
|
+
if (controlFlow.type === "if_statement" || controlFlow.type === "if_expression")
|
|
8049
|
+
return guardConditions(controlFlow, node).length > 0;
|
|
8050
|
+
if (controlFlow.type === "for_statement")
|
|
8051
|
+
return controlFlow.namedChildren.some((child) => child.type === "block" && containsNode(child, node));
|
|
8052
|
+
if (CONDITIONAL_TYPES.has(controlFlow.type))
|
|
8053
|
+
return true;
|
|
8054
|
+
if (controlFlow.type !== "binary_expression" || !controlFlow.children.some((child) => child.type === "and" || child.type === "or"))
|
|
8055
|
+
return false;
|
|
8056
|
+
const right = controlFlow.childForFieldName("right") ?? controlFlow.namedChildren[1];
|
|
8057
|
+
return Boolean(right && containsNode(right, node));
|
|
8058
|
+
}
|
|
8059
|
+
function simpleConditionRoot(condition) {
|
|
7821
8060
|
const text = condition.text.trim();
|
|
7822
8061
|
const match = text.match(/^(?:not\s+)?([A-Za-z_][A-Za-z0-9_]*)(?:\s*(?:==|~=)\s*(?:true|false|nil))?$/);
|
|
7823
8062
|
return match?.[1] ?? null;
|
|
7824
8063
|
}
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
8064
|
+
var unstableLocalNamesCache = new WeakMap;
|
|
8065
|
+
function unstableLocalNames(fn, context) {
|
|
8066
|
+
const cached = unstableLocalNamesCache.get(fn);
|
|
8067
|
+
if (cached)
|
|
8068
|
+
return cached;
|
|
8069
|
+
const declared = new Set;
|
|
8070
|
+
const unstable = new Set;
|
|
8071
|
+
for (const node of context.walk(fn.node)) {
|
|
8072
|
+
if (node.type === "variable_declaration" && context.nearestFunction(node) === fn) {
|
|
8073
|
+
for (const name of declarationNames2(node)) {
|
|
8074
|
+
if (declared.has(name))
|
|
8075
|
+
unstable.add(name);
|
|
8076
|
+
declared.add(name);
|
|
8077
|
+
}
|
|
8078
|
+
}
|
|
8079
|
+
if (node.type !== "assignment_statement" || node.parent?.type === "variable_declaration")
|
|
8080
|
+
continue;
|
|
8081
|
+
const variables = node.namedChildren.find((child) => child.type === "variable_list");
|
|
8082
|
+
for (const target of variables?.namedChildren ?? []) {
|
|
8083
|
+
if (target.type === "identifier")
|
|
8084
|
+
unstable.add(target.text);
|
|
8085
|
+
}
|
|
8086
|
+
}
|
|
8087
|
+
unstableLocalNamesCache.set(fn, unstable);
|
|
8088
|
+
return unstable;
|
|
8089
|
+
}
|
|
8090
|
+
function moduleNameIsUnshadowed(name, node, fn, context) {
|
|
8091
|
+
let owner = fn;
|
|
8092
|
+
while (owner) {
|
|
8093
|
+
if (owner.parameters.includes(name) || isBindingShadowedBetween(node, owner, name))
|
|
8094
|
+
return false;
|
|
8095
|
+
owner = context.nearestFunction(owner.node);
|
|
8096
|
+
}
|
|
8097
|
+
return true;
|
|
8098
|
+
}
|
|
8099
|
+
function conditionIsProvablyInvariant(controlFlow, fn, context, moduleInvariants, node = controlFlow) {
|
|
8100
|
+
if (!["if_statement", "if_expression", "binary_expression"].includes(controlFlow.type))
|
|
7828
8101
|
return false;
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
const root = simpleConditionRoot(controlFlow);
|
|
7832
|
-
if (!root)
|
|
8102
|
+
const conditions = guardConditions(controlFlow, node);
|
|
8103
|
+
if (conditions.length === 0)
|
|
7833
8104
|
return false;
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
8105
|
+
return conditions.every((condition) => {
|
|
8106
|
+
if (["true", "false", "nil", "number", "string"].includes(condition.type))
|
|
8107
|
+
return true;
|
|
8108
|
+
const root = simpleConditionRoot(condition);
|
|
8109
|
+
if (!root)
|
|
8110
|
+
return false;
|
|
8111
|
+
if (moduleInvariants.has(root) && moduleNameIsUnshadowed(root, condition, fn, context))
|
|
8112
|
+
return true;
|
|
8113
|
+
if (isNameShadowedBetween(condition, fn, root))
|
|
8114
|
+
return false;
|
|
8115
|
+
if (unstableLocalNames(fn, context).has(root))
|
|
8116
|
+
return false;
|
|
8117
|
+
return Boolean(context.model.stableVariablesByFunction.get(nodeKey(fn.node))?.has(root));
|
|
8118
|
+
});
|
|
7837
8119
|
}
|
|
7838
8120
|
function branchHookSequence(branch, fn, context) {
|
|
7839
8121
|
const hooks = [];
|
|
@@ -7848,28 +8130,45 @@ function branchHookSequence(branch, fn, context) {
|
|
|
7848
8130
|
const path = context.resolveCallPath(rawPath);
|
|
7849
8131
|
if (!isHookPath(path))
|
|
7850
8132
|
continue;
|
|
7851
|
-
const nestedControl = findAncestorBetween(node, branch, (ancestor) =>
|
|
7852
|
-
if (nestedControl)
|
|
8133
|
+
const nestedControl = node.id === branch.id ? null : findAncestorBetween(node, branch, (ancestor) => conditionallyContains(ancestor, node));
|
|
8134
|
+
if (nestedControl || conditionallyContains(branch, node))
|
|
7853
8135
|
return null;
|
|
7854
8136
|
hooks.push(path);
|
|
7855
8137
|
}
|
|
7856
8138
|
return hooks;
|
|
7857
8139
|
}
|
|
7858
8140
|
function ifBranchesHaveEquivalentBuiltInTopology(controlFlow, fn, context) {
|
|
7859
|
-
|
|
8141
|
+
const isStatement = controlFlow.type === "if_statement";
|
|
8142
|
+
if (!isStatement && controlFlow.type !== "if_expression")
|
|
7860
8143
|
return false;
|
|
7861
|
-
const
|
|
7862
|
-
const
|
|
7863
|
-
const
|
|
7864
|
-
if (!
|
|
8144
|
+
const firstBranch = isStatement ? controlFlow.namedChildren.find((child) => child.type === "block") : controlFlow.namedChildren[1];
|
|
8145
|
+
const elseClause = controlFlow.namedChildren.find((child) => child.type === "else_statement" || child.type === "else_clause");
|
|
8146
|
+
const lastBranch = isStatement ? elseClause?.namedChildren.find((child) => child.type === "block") : elseClause?.namedChildren[0];
|
|
8147
|
+
if (!firstBranch || !lastBranch)
|
|
7865
8148
|
return false;
|
|
7866
|
-
const
|
|
7867
|
-
const
|
|
7868
|
-
|
|
8149
|
+
const branches = [firstBranch];
|
|
8150
|
+
for (const clause of controlFlow.namedChildren) {
|
|
8151
|
+
if (clause.type !== "elseif_statement" && clause.type !== "elseif_clause")
|
|
8152
|
+
continue;
|
|
8153
|
+
const condition = clause.namedChildren[0];
|
|
8154
|
+
if (!condition)
|
|
8155
|
+
return false;
|
|
8156
|
+
const conditionHooks = branchHookSequence(condition, fn, context);
|
|
8157
|
+
if (!conditionHooks || conditionHooks.length > 0)
|
|
8158
|
+
return false;
|
|
8159
|
+
const branch = isStatement ? clause.namedChildren.find((child) => child.type === "block") : clause.namedChildren[1];
|
|
8160
|
+
if (!branch)
|
|
8161
|
+
return false;
|
|
8162
|
+
branches.push(branch);
|
|
8163
|
+
}
|
|
8164
|
+
branches.push(lastBranch);
|
|
8165
|
+
const sequences = branches.map((branch) => branchHookSequence(branch, fn, context));
|
|
8166
|
+
const first = sequences[0];
|
|
8167
|
+
if (!first)
|
|
7869
8168
|
return false;
|
|
7870
|
-
if (!
|
|
8169
|
+
if (!sequences.every((sequence) => sequence && sequence.length === first.length && sequence.every((path, index) => path === first[index])))
|
|
7871
8170
|
return false;
|
|
7872
|
-
return
|
|
8171
|
+
return first.every((path) => path.startsWith("React."));
|
|
7873
8172
|
}
|
|
7874
8173
|
function stateModeStability(name, owner, context) {
|
|
7875
8174
|
const binding = context.model.stateBindings.find((candidate) => candidate.owner === owner && candidate.valueName === name);
|
|
@@ -7974,7 +8273,21 @@ function controlledAccessPaths(summary, parameterIndex) {
|
|
|
7974
8273
|
paths.add("");
|
|
7975
8274
|
return [...paths];
|
|
7976
8275
|
}
|
|
7977
|
-
function loopIterationIsProvablyStable(loop, imports, owner, stableShapes, moduleTables) {
|
|
8276
|
+
function loopIterationIsProvablyStable(loop, imports, owner, stableShapes, moduleTables, context, moduleInvariants) {
|
|
8277
|
+
for (const node of context.walk(loop)) {
|
|
8278
|
+
if (!["break_statement", "continue_statement", "return_statement"].includes(node.type))
|
|
8279
|
+
continue;
|
|
8280
|
+
if (context.nearestFunction(node) !== owner)
|
|
8281
|
+
continue;
|
|
8282
|
+
if (node.type !== "return_statement") {
|
|
8283
|
+
const targetLoop = findAncestorBetween(node, owner.node, (ancestor) => ["for_statement", "while_statement", "repeat_statement"].includes(ancestor.type));
|
|
8284
|
+
if (targetLoop?.id !== loop.id)
|
|
8285
|
+
continue;
|
|
8286
|
+
}
|
|
8287
|
+
const dynamicGuard = findAncestorBetween(node, loop, (ancestor) => conditionallyContains(ancestor, node) && !conditionIsProvablyInvariant(ancestor, owner, context, moduleInvariants, node));
|
|
8288
|
+
if (dynamicGuard)
|
|
8289
|
+
return false;
|
|
8290
|
+
}
|
|
7978
8291
|
const numeric = loop.namedChildren.find((child) => child.type === "for_numeric_clause");
|
|
7979
8292
|
if (numeric) {
|
|
7980
8293
|
const text = numeric.text;
|
|
@@ -8047,23 +8360,46 @@ function reachableReturnsBefore(call, fn, context, cache) {
|
|
|
8047
8360
|
returns = [...context.walk(fn.body)].filter((node) => node.type === "return_statement" && !isNestedInsideFunction(node, fn.node) && !impossibleNilGuardReturn(node, fn, parameterTypes));
|
|
8048
8361
|
cache.set(key, returns);
|
|
8049
8362
|
}
|
|
8050
|
-
return returns.filter((node) =>
|
|
8363
|
+
return returns.filter((node) => {
|
|
8364
|
+
if (node.endIndex > call.startIndex)
|
|
8365
|
+
return false;
|
|
8366
|
+
return !findAncestorBetween(node, fn.node, (ancestor) => {
|
|
8367
|
+
if (ancestor.type !== "if_statement")
|
|
8368
|
+
return false;
|
|
8369
|
+
const returnBranch = branchContaining(ancestor, node);
|
|
8370
|
+
const callBranch = branchContaining(ancestor, call);
|
|
8371
|
+
return Boolean(returnBranch && callBranch && returnBranch.id !== callBranch.id);
|
|
8372
|
+
});
|
|
8373
|
+
});
|
|
8051
8374
|
}
|
|
8052
8375
|
function returnIsControlledByStableTopologyMode(node, fn, summary, context, moduleInvariants) {
|
|
8053
8376
|
let current = node.parent;
|
|
8377
|
+
let sawStableGuard = false;
|
|
8054
8378
|
while (current && current !== fn.node) {
|
|
8055
|
-
if (
|
|
8056
|
-
if (summary && controlledParameterIndex(current, fn, summary) !== null)
|
|
8057
|
-
return true;
|
|
8058
|
-
if (conditionIsProvablyInvariant(current, fn, context, moduleInvariants))
|
|
8059
|
-
return true;
|
|
8379
|
+
if (["for_statement", "while_statement", "repeat_statement"].includes(current.type))
|
|
8060
8380
|
return false;
|
|
8381
|
+
if (current.type === "if_statement" || current.type === "if_expression") {
|
|
8382
|
+
if (summary && controlledParameterIndex(current, fn, summary, node) !== null)
|
|
8383
|
+
sawStableGuard = true;
|
|
8384
|
+
else if (conditionIsProvablyInvariant(current, fn, context, moduleInvariants, node))
|
|
8385
|
+
sawStableGuard = true;
|
|
8386
|
+
else
|
|
8387
|
+
return false;
|
|
8061
8388
|
}
|
|
8062
8389
|
current = current.parent;
|
|
8063
8390
|
}
|
|
8064
|
-
return
|
|
8391
|
+
return sawStableGuard;
|
|
8065
8392
|
}
|
|
8066
8393
|
function conditionalFixPreview(kind, hookName) {
|
|
8394
|
+
if (kind === "short-circuit expression") {
|
|
8395
|
+
return {
|
|
8396
|
+
kind: "pattern",
|
|
8397
|
+
before: `local result = enabled and renderChild(${hookName}())`,
|
|
8398
|
+
after: `local value = ${hookName}()
|
|
8399
|
+
local result = enabled and renderChild(value)`,
|
|
8400
|
+
note: "Call the hook before the short-circuit expression, then branch on its result"
|
|
8401
|
+
};
|
|
8402
|
+
}
|
|
8067
8403
|
if (hookName === "React.useEffect" && kind === "if statement") {
|
|
8068
8404
|
return {
|
|
8069
8405
|
kind: "pattern",
|
|
@@ -8142,6 +8478,34 @@ end`,
|
|
|
8142
8478
|
note: "Call hooks before any reachable early return so every render executes the same hook sequence."
|
|
8143
8479
|
};
|
|
8144
8480
|
}
|
|
8481
|
+
function callbackFunctions(context) {
|
|
8482
|
+
const callbacks = new Set;
|
|
8483
|
+
const addCallback = (expression) => {
|
|
8484
|
+
const fn = resolveLocalFunction(context, expression);
|
|
8485
|
+
if (fn)
|
|
8486
|
+
callbacks.add(fn.node.id);
|
|
8487
|
+
};
|
|
8488
|
+
for (const call of context.findCalls()) {
|
|
8489
|
+
const path = reactApiPath(context, call);
|
|
8490
|
+
const args = context.callArguments(call);
|
|
8491
|
+
for (const [index, argument] of args.entries()) {
|
|
8492
|
+
if (index === 0 && ["React.memo", "React.forwardRef", "React.createElement"].includes(path ?? ""))
|
|
8493
|
+
continue;
|
|
8494
|
+
if (path?.startsWith("React.use") || path === "React.memo" || unwrapExpression(argument).type === "function_definition")
|
|
8495
|
+
addCallback(argument);
|
|
8496
|
+
}
|
|
8497
|
+
if (path !== "React.createElement" || args[0]?.type !== "string" || args[1]?.type !== "table_constructor")
|
|
8498
|
+
continue;
|
|
8499
|
+
for (const field of args[1].namedChildren) {
|
|
8500
|
+
if (field.type !== "field")
|
|
8501
|
+
continue;
|
|
8502
|
+
const value = field.namedChildren.at(-1);
|
|
8503
|
+
if (value)
|
|
8504
|
+
addCallback(value);
|
|
8505
|
+
}
|
|
8506
|
+
}
|
|
8507
|
+
return callbacks;
|
|
8508
|
+
}
|
|
8145
8509
|
var rulesOfHooks = {
|
|
8146
8510
|
id: "react-luau/rules-of-hooks",
|
|
8147
8511
|
category: "Hooks",
|
|
@@ -8162,6 +8526,7 @@ var rulesOfHooks = {
|
|
|
8162
8526
|
const modeImports = conditionalHookModeImports(context);
|
|
8163
8527
|
const ignoredHookImports = nonReactHookImports(context);
|
|
8164
8528
|
const currentModeSummary = currentConditionalHookMode(context);
|
|
8529
|
+
const callbacks = callbackFunctions(context);
|
|
8165
8530
|
const reachableReturns = new Map;
|
|
8166
8531
|
for (const call of context.findCalls()) {
|
|
8167
8532
|
const rawPath = context.getCallPath(call);
|
|
@@ -8173,11 +8538,11 @@ var rulesOfHooks = {
|
|
|
8173
8538
|
if (!isHookPath(path))
|
|
8174
8539
|
continue;
|
|
8175
8540
|
const fn = context.nearestFunction(call);
|
|
8176
|
-
if (!fn || !fn.isComponent && !fn.isHook) {
|
|
8541
|
+
if (!fn || !fn.isComponent && !fn.isHook || callbacks.has(fn.node.id)) {
|
|
8177
8542
|
diagnostics.push({
|
|
8178
8543
|
node: callNameNode(call),
|
|
8179
|
-
message: `Hook ${path} is called outside a React component or custom hook
|
|
8180
|
-
help: "Move the hook into a component or a custom hook whose name starts with use
|
|
8544
|
+
message: `Hook ${path} is called outside a React component or custom hook`,
|
|
8545
|
+
help: "Move the hook into a component or a custom hook whose name starts with use",
|
|
8181
8546
|
fixPreview: {
|
|
8182
8547
|
kind: "pattern",
|
|
8183
8548
|
before: `local value = ${path}()
|
|
@@ -8189,7 +8554,7 @@ end`,
|
|
|
8189
8554
|
local value = ${path}()
|
|
8190
8555
|
return value
|
|
8191
8556
|
end`,
|
|
8192
|
-
note: "Hooks need a React-owned component or custom-hook call stack
|
|
8557
|
+
note: "Hooks need a React-owned component or custom-hook call stack"
|
|
8193
8558
|
}
|
|
8194
8559
|
});
|
|
8195
8560
|
continue;
|
|
@@ -8220,24 +8585,28 @@ end`,
|
|
|
8220
8585
|
});
|
|
8221
8586
|
}
|
|
8222
8587
|
}
|
|
8223
|
-
const controlFlow = findAncestorBetween(call, fn.node, (node) =>
|
|
8588
|
+
const controlFlow = findAncestorBetween(call, fn.node, (node) => {
|
|
8589
|
+
if (!conditionallyContains(node, call))
|
|
8590
|
+
return false;
|
|
8591
|
+
if (currentModeSummary && controlledParameterIndex(node, fn, currentModeSummary, call) !== null)
|
|
8592
|
+
return false;
|
|
8593
|
+
if (conditionIsProvablyInvariant(node, fn, context, moduleInvariants, call))
|
|
8594
|
+
return false;
|
|
8595
|
+
if (ifBranchesHaveEquivalentBuiltInTopology(node, fn, context))
|
|
8596
|
+
return false;
|
|
8597
|
+
if (node.type === "for_statement" && loopIterationIsProvablyStable(node, staticImports, fn, stableShapes, moduleTables, context, moduleInvariants))
|
|
8598
|
+
return false;
|
|
8599
|
+
return true;
|
|
8600
|
+
});
|
|
8224
8601
|
if (controlFlow) {
|
|
8225
|
-
|
|
8226
|
-
continue;
|
|
8227
|
-
if (conditionIsProvablyInvariant(controlFlow, fn, context, moduleInvariants))
|
|
8228
|
-
continue;
|
|
8229
|
-
if (ifBranchesHaveEquivalentBuiltInTopology(controlFlow, fn, context))
|
|
8230
|
-
continue;
|
|
8231
|
-
if (controlFlow.type === "for_statement" && loopIterationIsProvablyStable(controlFlow, staticImports, fn, stableShapes, moduleTables))
|
|
8232
|
-
continue;
|
|
8233
|
-
const kind = controlFlow.type.replaceAll("_", " ");
|
|
8602
|
+
const kind = controlFlow.type === "binary_expression" ? "short-circuit expression" : controlFlow.type.replaceAll("_", " ");
|
|
8234
8603
|
const loopLike = controlFlow.type === "for_statement" || controlFlow.type === "while_statement" || controlFlow.type === "repeat_statement";
|
|
8235
8604
|
diagnostics.push({
|
|
8236
8605
|
node: callNameNode(call),
|
|
8237
|
-
message: loopLike ? `Hook ${path} is called inside ${kind} whose size or iteration order may change between renders. If that happens, React will see a different hook sequence
|
|
8238
|
-
summary: loopLike ? `${path} may run a different number of times between renders
|
|
8239
|
-
explanation: loopLike ? `This ${kind} may change how many times ${path} is called
|
|
8240
|
-
help: loopLike ? "Keep hook count independent of runtime collection size or loop iterations. A common fix is to render a child component per item and call the hook inside that child
|
|
8606
|
+
message: loopLike ? `Hook ${path} is called inside ${kind} whose size or iteration order may change between renders. If that happens, React will see a different hook sequence` : `Hook ${path} is called inside ${kind}, so the hook may be skipped on some renders`,
|
|
8607
|
+
summary: loopLike ? `${path} may run a different number of times between renders` : `${path} may be skipped on some renders`,
|
|
8608
|
+
explanation: loopLike ? `This ${kind} may change how many times ${path} is called` : `This ${kind} can skip ${path} on some renders`,
|
|
8609
|
+
help: loopLike ? "Keep hook count independent of runtime collection size or loop iterations. A common fix is to render a child component per item and call the hook inside that child" : "Call the hook unconditionally at the top level, then branch on the returned value",
|
|
8241
8610
|
fixPreview: conditionalFixPreview(kind, path)
|
|
8242
8611
|
});
|
|
8243
8612
|
continue;
|
|
@@ -8610,7 +8979,7 @@ var unstableContextValue = {
|
|
|
8610
8979
|
id: "react-luau/unstable-context-value",
|
|
8611
8980
|
category: "Performance",
|
|
8612
8981
|
severity: "warning",
|
|
8613
|
-
description: "Avoid recreating context
|
|
8982
|
+
description: "Avoid recreating context reference values during render when identity matters",
|
|
8614
8983
|
run(context) {
|
|
8615
8984
|
const diagnostics = [];
|
|
8616
8985
|
for (const call of context.findCalls()) {
|
|
@@ -8625,13 +8994,13 @@ var unstableContextValue = {
|
|
|
8625
8994
|
const props = args[1];
|
|
8626
8995
|
if (!/\.Provider$/.test(element) || !props || props.type !== "table_constructor")
|
|
8627
8996
|
continue;
|
|
8628
|
-
const valueField = props.namedChildren.find((field) => field.type === "field" && fieldName(field) === "value" && fieldValue(field)
|
|
8997
|
+
const valueField = props.namedChildren.find((field) => field.type === "field" && fieldName(field) === "value" && freshValueKind(context, fieldValue(field), component));
|
|
8629
8998
|
if (!valueField)
|
|
8630
8999
|
continue;
|
|
8631
9000
|
diagnostics.push({
|
|
8632
9001
|
node: fieldNameNode(valueField),
|
|
8633
|
-
message: "Context Provider
|
|
8634
|
-
help: "Memoize the provider value from its real dependencies when consumer updates depend on object identity
|
|
9002
|
+
message: "Context Provider can receive a new reference value when the component renders",
|
|
9003
|
+
help: "Memoize the provider value from its real dependencies when consumer updates depend on object identity"
|
|
8635
9004
|
});
|
|
8636
9005
|
}
|
|
8637
9006
|
return diagnostics;
|
|
@@ -9979,5 +10348,5 @@ parentPort.on("message", async (request) => {
|
|
|
9979
10348
|
}
|
|
9980
10349
|
});
|
|
9981
10350
|
|
|
9982
|
-
//# debugId=
|
|
10351
|
+
//# debugId=AB0DC00E72A2325A64756E2164756E21
|
|
9983
10352
|
//# sourceMappingURL=scan-worker.js.map
|