@colrealpro/react-luau-doctor 0.20.0 → 0.20.2

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.
@@ -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];
@@ -1162,7 +1168,13 @@ function isLazyRefInitialization(context, call) {
1162
1168
  }
1163
1169
  return false;
1164
1170
  }
1165
- function isRefInitializer(context, call) {
1171
+ var hookInitializerArguments = new Map([
1172
+ ["React.useRef", 0],
1173
+ ["React.useState", 0],
1174
+ ["React.useBinding", 0],
1175
+ ["React.useReducer", 1]
1176
+ ]);
1177
+ function isHookInitializer(context, call) {
1166
1178
  let node = call;
1167
1179
  while (node.parent) {
1168
1180
  const parent = node.parent;
@@ -1170,8 +1182,11 @@ function isRefInitializer(context, call) {
1170
1182
  return false;
1171
1183
  if (parent.type === "arguments") {
1172
1184
  const hook = parent.parent;
1173
- if (hook?.type === "function_call" && reactApiPath(context, hook) === "React.useRef" && context.callArguments(hook)[0]?.id === node.id)
1174
- return true;
1185
+ if (hook?.type === "function_call") {
1186
+ const argumentIndex = hookInitializerArguments.get(reactApiPath(context, hook) ?? "");
1187
+ if (argumentIndex !== undefined && context.callArguments(hook)[argumentIndex]?.id === node.id)
1188
+ return true;
1189
+ }
1175
1190
  }
1176
1191
  node = parent;
1177
1192
  }
@@ -1205,18 +1220,18 @@ var noBindingGetValueInRender = {
1205
1220
  id: "react-luau/no-binding-getvalue-in-render",
1206
1221
  category: "Correctness",
1207
1222
  severity: "warning",
1208
- description: "Do not read binding snapshots during render except to initialize a ref",
1223
+ description: "Do not read binding snapshots during render except in hook initialization arguments",
1209
1224
  guidance: {
1210
1225
  explanation: "getValue returns a snapshot without subscribing \u2014 later binding updates do not rerender the component or update props built from that snapshot",
1211
1226
  help: "Pass the binding directly to the prop or use binding:map(function(value) return ... end)",
1212
- caveat: "A snapshot used to initialize React.useRef is allowed because the initial value is intentionally preserved"
1227
+ caveat: "Snapshots in the first argument to React.useRef, React.useState, or React.useBinding, or the second argument to React.useReducer, are allowed because those arguments are used only for initialization"
1213
1228
  },
1214
1229
  run(context) {
1215
1230
  return context.findCalls().flatMap((call) => {
1216
1231
  const owner = context.nearestFunction(call);
1217
1232
  const name = call.childForFieldName("name");
1218
1233
  const receiver = name?.namedChildren[0];
1219
- if (!owner || !owner.isComponent && !owner.isHook || name?.type !== "method_index_expression" || name.namedChildren.at(-1)?.text !== "getValue" || !receiver || !isBinding(context, receiver) || isRefInitializer(context, call))
1234
+ if (!owner || !owner.isComponent && !owner.isHook || name?.type !== "method_index_expression" || name.namedChildren.at(-1)?.text !== "getValue" || !receiver || !isBinding(context, receiver) || isHookInitializer(context, call))
1220
1235
  return [];
1221
1236
  return [
1222
1237
  {
@@ -2980,6 +2995,242 @@ var effectNeedsCleanup = {
2980
2995
  }
2981
2996
  };
2982
2997
 
2998
+ // src/api-reference.ts
2999
+ var functions = new Map;
3000
+ function register(namespace, members, reference) {
3001
+ for (const member of members)
3002
+ functions.set(`${namespace}.${member}`, reference);
3003
+ }
3004
+ for (const type of [
3005
+ "Instance",
3006
+ "Random",
3007
+ "RaycastParams",
3008
+ "OverlapParams",
3009
+ "CatalogSearchParams",
3010
+ "SharedTable"
3011
+ ]) {
3012
+ register(type, ["new"], {
3013
+ returnType: type,
3014
+ allocation: "new",
3015
+ equality: "reference"
3016
+ });
3017
+ }
3018
+ register("table", ["clone", "create", "pack"], {
3019
+ returnType: "table",
3020
+ allocation: "new",
3021
+ equality: "reference"
3022
+ });
3023
+ register("SharedTable", ["clone", "cloneAndFreeze"], {
3024
+ returnType: "SharedTable",
3025
+ allocation: "new",
3026
+ equality: "reference"
3027
+ });
3028
+ register("string", ["split"], {
3029
+ returnType: "table",
3030
+ allocation: "new",
3031
+ equality: "reference"
3032
+ });
3033
+ register("buffer", ["create", "fromstring"], {
3034
+ returnType: "buffer",
3035
+ allocation: "new",
3036
+ equality: "reference"
3037
+ });
3038
+ register("coroutine", ["create"], {
3039
+ returnType: "thread",
3040
+ allocation: "new",
3041
+ equality: "reference"
3042
+ });
3043
+ register("coroutine", ["wrap"], {
3044
+ returnType: "function",
3045
+ allocation: "new",
3046
+ equality: "reference"
3047
+ });
3048
+ register("table", ["freeze"], {
3049
+ returnType: "table",
3050
+ allocation: "argument",
3051
+ equality: "reference",
3052
+ argumentIndex: 0
3053
+ });
3054
+ for (const [type, members] of Object.entries({
3055
+ Color3: ["new", "fromRGB", "fromHSV", "fromHex"],
3056
+ ColorSequence: ["new"],
3057
+ ColorSequenceKeypoint: ["new"],
3058
+ NumberSequence: ["new"],
3059
+ NumberSequenceKeypoint: ["new"],
3060
+ NumberRange: ["new"],
3061
+ UDim: ["new"],
3062
+ UDim2: ["new", "fromScale", "fromOffset"],
3063
+ Vector2: ["new"],
3064
+ Vector3: ["new", "fromNormalId", "fromAxis"],
3065
+ CFrame: [
3066
+ "new",
3067
+ "Angles",
3068
+ "fromEulerAnglesXYZ",
3069
+ "fromEulerAnglesYXZ",
3070
+ "fromOrientation",
3071
+ "fromAxisAngle",
3072
+ "fromMatrix",
3073
+ "lookAt",
3074
+ "lookAlong"
3075
+ ],
3076
+ Rect: ["new"],
3077
+ BrickColor: ["new", "palette"]
3078
+ })) {
3079
+ register(type, members, {
3080
+ returnType: type,
3081
+ allocation: "new",
3082
+ equality: "value"
3083
+ });
3084
+ }
3085
+ var instanceMethods = new Map([
3086
+ ...["GetChildren", "GetDescendants", "GetAttributes", "GetTags"].map((member) => [
3087
+ member,
3088
+ { returnType: "table", allocation: "new", equality: "reference" }
3089
+ ]),
3090
+ [
3091
+ "Clone",
3092
+ { returnType: "Instance", allocation: "new", equality: "reference" }
3093
+ ]
3094
+ ]);
3095
+ function apiFunctionReference(path) {
3096
+ return functions.get(path) ?? null;
3097
+ }
3098
+ function globalPath(context, expression, seen = new Set) {
3099
+ const node = unwrapExpression(expression);
3100
+ if (seen.has(node.id))
3101
+ return null;
3102
+ seen.add(node.id);
3103
+ if (node.type === "identifier") {
3104
+ if (isUnboundIdentifier(context, node))
3105
+ return node.text;
3106
+ const binding = localValueBinding(context, node);
3107
+ return binding?.returnIndex === 0 ? globalPath(context, binding.value, seen) : null;
3108
+ }
3109
+ if (node.type !== "dot_index_expression")
3110
+ return null;
3111
+ const namespace = node.childForFieldName("table");
3112
+ const member = node.childForFieldName("field");
3113
+ const path = namespace && globalPath(context, namespace, seen);
3114
+ return path && member ? `${path}.${member.text}` : null;
3115
+ }
3116
+ function isInstanceReceiver(context, expression, seen = new Set) {
3117
+ const node = unwrapExpression(expression);
3118
+ if (seen.has(node.id))
3119
+ return false;
3120
+ seen.add(node.id);
3121
+ if (node.type === "identifier") {
3122
+ if (isUnboundIdentifier(context, node))
3123
+ return ["game", "workspace", "script"].includes(node.text);
3124
+ const binding = localValueBinding(context, node);
3125
+ return binding?.returnIndex === 0 && isInstanceReceiver(context, binding.value, seen);
3126
+ }
3127
+ if (node.type === "dot_index_expression") {
3128
+ const receiver = node.childForFieldName("table");
3129
+ return Boolean(receiver && isInstanceReceiver(context, receiver, seen));
3130
+ }
3131
+ if (node.type !== "function_call")
3132
+ return false;
3133
+ const name = node.childForFieldName("name");
3134
+ if (!name)
3135
+ return false;
3136
+ if (globalPath(context, name) === "Instance.new")
3137
+ return true;
3138
+ if (name.type !== "method_index_expression")
3139
+ return false;
3140
+ const receiver = name.childForFieldName("table");
3141
+ const member = name.childForFieldName("method")?.text;
3142
+ return Boolean(receiver && member && [
3143
+ "GetService",
3144
+ "WaitForChild",
3145
+ "FindFirstChild",
3146
+ "FindFirstAncestor",
3147
+ "Clone"
3148
+ ].includes(member) && isInstanceReceiver(context, receiver, seen));
3149
+ }
3150
+ function apiReferenceForCall(context, call) {
3151
+ const name = call.childForFieldName("name");
3152
+ if (!name)
3153
+ return null;
3154
+ const path = globalPath(context, name);
3155
+ if (path)
3156
+ return apiFunctionReference(path);
3157
+ if (name.type !== "method_index_expression")
3158
+ return null;
3159
+ const receiver = name.childForFieldName("table");
3160
+ const member = name.childForFieldName("method")?.text;
3161
+ return receiver && member && isInstanceReceiver(context, receiver) ? instanceMethods.get(member) ?? null : null;
3162
+ }
3163
+
3164
+ // src/ast/value-identity.ts
3165
+ function freshValueKind(context, expression, lifetime, seen = new Set, returnIndex = 0, lifetimes = new Set([lifetime])) {
3166
+ if (!expression)
3167
+ return null;
3168
+ const node = unwrapExpression(expression);
3169
+ if (seen.has(node.id) || seen.size >= 64)
3170
+ return null;
3171
+ const visited = new Set(seen).add(node.id);
3172
+ const analyze = (value, owner = lifetime, index = 0) => freshValueKind(context, value, owner, visited, index, new Set(lifetimes).add(owner));
3173
+ if (node.type === "identifier") {
3174
+ const binding = resolveLocalValue(context, node);
3175
+ const owner = binding && context.nearestFunction(binding.value);
3176
+ if (!binding || !owner || !lifetimes.has(owner))
3177
+ return null;
3178
+ return analyze(binding.value, lifetime, binding.returnIndex);
3179
+ }
3180
+ if (returnIndex === 0) {
3181
+ if (node.type === "table_constructor")
3182
+ return "table";
3183
+ if (node.type === "function_definition" || node.type === "function_declaration")
3184
+ return "function";
3185
+ if (node.type === "binary_expression") {
3186
+ const operator = node.children.find((child) => ["and", "or"].includes(child.type))?.type;
3187
+ if (operator === "and")
3188
+ return analyze(node.childForFieldName("right"));
3189
+ if (operator === "or")
3190
+ return analyze(node.childForFieldName("left")) ?? analyze(node.childForFieldName("right"));
3191
+ }
3192
+ if (node.type === "if_expression") {
3193
+ for (const branch of [
3194
+ node,
3195
+ ...node.namedChildren.filter((child) => ["elseif_clause", "else_clause"].includes(child.type))
3196
+ ]) {
3197
+ const kind = analyze(branch.childForFieldName("consequence"));
3198
+ if (kind)
3199
+ return kind;
3200
+ }
3201
+ }
3202
+ }
3203
+ if (node.type !== "function_call")
3204
+ return null;
3205
+ const api = apiReferenceForCall(context, node);
3206
+ if (api && returnIndex === 0) {
3207
+ if (api.allocation === "argument")
3208
+ return analyze(context.callArguments(node)[api.argumentIndex ?? 0] ?? null);
3209
+ if (api.equality !== "reference")
3210
+ return null;
3211
+ return api.returnType === "table" || api.returnType === "function" ? api.returnType : "object";
3212
+ }
3213
+ const name = node.childForFieldName("name");
3214
+ const factory = name && resolveLocalFunction(context, name);
3215
+ if (!factory?.body)
3216
+ return null;
3217
+ for (const statement of context.walk(factory.body)) {
3218
+ if (statement.type !== "return_statement" || context.nearestFunction(statement) !== factory)
3219
+ continue;
3220
+ const values = statement.namedChildren.find((child) => child.type === "expression_list")?.namedChildren ?? [];
3221
+ const value = values[returnIndex] ?? values.at(-1);
3222
+ if (!value)
3223
+ continue;
3224
+ const index = Math.max(0, returnIndex - values.length + 1);
3225
+ if (index > 0 && value.type !== "function_call")
3226
+ continue;
3227
+ const kind = analyze(value, factory, index);
3228
+ if (kind)
3229
+ return kind;
3230
+ }
3231
+ return null;
3232
+ }
3233
+
2983
3234
  // src/rules/effect-state-rules.ts
2984
3235
  var HOOKS_WITH_DEPS = new Set([
2985
3236
  "React.useEffect",
@@ -3043,23 +3294,6 @@ function hasExternalSubscription(node, context) {
3043
3294
  }
3044
3295
  return false;
3045
3296
  }
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
3297
  function setterCalledOutsideCallback(context, owner, callback, setterName) {
3064
3298
  if (!owner.body)
3065
3299
  return false;
@@ -3099,20 +3333,6 @@ function guaranteedRepeatedStateChange(argument, stateName, context) {
3099
3333
  }
3100
3334
  return false;
3101
3335
  }
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
3336
  var noDerivedStateEffect = {
3117
3337
  id: "react-luau/no-derived-state-effect",
3118
3338
  category: "Effects",
@@ -3224,7 +3444,7 @@ var noEffectWithFreshDeps = {
3224
3444
  id: "react-luau/no-effect-with-fresh-deps",
3225
3445
  category: "Hooks",
3226
3446
  severity: "error",
3227
- description: "Dependency tables should not contain tables or functions recreated on every render.",
3447
+ description: "Dependency tables should not contain reference values recreated during render",
3228
3448
  run(context) {
3229
3449
  const diagnostics = [];
3230
3450
  for (const call of context.findCalls()) {
@@ -3236,14 +3456,14 @@ var noEffectWithFreshDeps = {
3236
3456
  if (!owner || !parts)
3237
3457
  continue;
3238
3458
  for (const dependency of dependencyExpressions(parts.deps)) {
3239
- const kind = freshDependencyKind(dependency, owner);
3459
+ const kind = freshValueKind(context, dependency, owner);
3240
3460
  if (!kind)
3241
3461
  continue;
3242
3462
  const label = dependency.type === "identifier" ? ` ${dependency.text}` : "";
3243
3463
  diagnostics.push({
3244
3464
  node: dependency,
3245
- message: `${path} depends on${label || " an inline value"} that is a new ${kind} on every render.`,
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."
3465
+ message: `${path} depends on${label || " an inline value"} that can be a new ${kind} when the component renders`,
3466
+ 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
3467
  });
3248
3468
  }
3249
3469
  }
@@ -5350,40 +5570,32 @@ function componentRenderCost(context, owner) {
5350
5570
  parts.push(`${discovery} Instance discovery call${discovery === 1 ? "" : "s"}`);
5351
5571
  return parts;
5352
5572
  }
5353
- function memoizedImports(context) {
5354
- const result = new Map;
5355
- for (const node of context.walk(context.root)) {
5356
- if (node.type !== "variable_declaration")
5357
- continue;
5358
- const match = node.text.match(/^\s*local\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*require\s*\((.*?)\)\s*$/s);
5359
- if (!match)
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);
5573
+ function memoizedComponentKind(context, expression) {
5574
+ const binding = resolveLocalValue(context, expression);
5575
+ if (!binding || binding.returnIndex !== 0 || binding.value.type !== "function_call")
5576
+ return null;
5577
+ const call = binding.value;
5578
+ if (reactApiPath(context, call) === "React.memo") {
5579
+ return context.callArguments(call).length > 1 ? "custom" : "shallow";
5365
5580
  }
5366
- return result;
5581
+ const target = context.callArguments(call)[0];
5582
+ if (context.getCallPath(call) !== "require" || !target)
5583
+ return null;
5584
+ return resolveModuleReference(normalizeRequireTarget(target.text), context.project.memoizedModules) ?? null;
5367
5585
  }
5368
5586
  function fieldLabel(field) {
5369
5587
  return fieldName(field) ?? field.childForFieldName("name")?.text ?? "prop";
5370
5588
  }
5371
- function isInlineFreshValue(node) {
5372
- return node?.type === "table_constructor" || node?.type === "function_definition";
5373
- }
5374
5589
  var rerenderUnstableMemoProps = {
5375
5590
  id: "react-luau/rerender-unstable-memo-props",
5376
5591
  category: "Performance",
5377
5592
  severity: "warning",
5378
- description: "Warn when fresh table or function props defeat shallow React.memo comparisons.",
5593
+ description: "Warn when props created during render can defeat shallow React.memo comparisons",
5379
5594
  guidance: {
5380
- explanation: "A new function or table has a new identity, so a shallow memo comparison cannot skip this child.",
5381
- help: "Stabilize props that change each render if this child is expensive, move construction into the child, or remove ineffective memoization."
5595
+ explanation: "New tables, functions, and reference objects compare unequal even when their contents match",
5596
+ help: "Stabilize props that change each render if this child is expensive, move construction into the child, or remove ineffective memoization"
5382
5597
  },
5383
5598
  run(context) {
5384
- const imports = memoizedImports(context);
5385
- if (imports.size === 0)
5386
- return [];
5387
5599
  const diagnostics = [];
5388
5600
  for (const call of context.findCalls()) {
5389
5601
  if (context.resolveCallPath(context.getCallPath(call) ?? "") !== "React.createElement")
@@ -5393,12 +5605,13 @@ var rerenderUnstableMemoProps = {
5393
5605
  continue;
5394
5606
  const args = context.callArguments(call);
5395
5607
  const childName = args[0]?.text.trim() ?? "";
5396
- if (imports.get(childName) !== "shallow")
5608
+ if (!args[0] || memoizedComponentKind(context, args[0]) !== "shallow")
5397
5609
  continue;
5398
- const props = args[1];
5399
- if (!props || props.type !== "table_constructor")
5610
+ const propsBinding = args[1] && resolveLocalValue(context, args[1]);
5611
+ const props = propsBinding?.returnIndex === 0 ? propsBinding.value : null;
5612
+ if (!props || props.type !== "table_constructor" || context.nearestFunction(props) !== component)
5400
5613
  continue;
5401
- const unstableFields = props.namedChildren.filter((node) => node.type === "field").filter((field) => isInlineFreshValue(fieldValue(field)));
5614
+ const unstableFields = props.namedChildren.filter((node) => node.type === "field").filter((field) => freshValueKind(context, fieldValue(field), component));
5402
5615
  if (unstableFields.length === 0)
5403
5616
  continue;
5404
5617
  const unstable = unstableFields.map(fieldLabel);
@@ -5407,13 +5620,13 @@ var rerenderUnstableMemoProps = {
5407
5620
  node: highlights[0],
5408
5621
  highlights,
5409
5622
  severity: "warning",
5410
- message: `Memoized ${childName} receives fresh ${unstable.join(", ")} prop${unstable.length === 1 ? "" : "s"} on every parent render.`,
5411
- summary: `${childName} gets ${unstable.length} fresh ${unstable.length === 1 ? "prop" : "props"} each render. Memoization cannot skip it.`,
5623
+ message: `Memoized ${childName} can receive fresh ${unstable.join(", ")} prop${unstable.length === 1 ? "" : "s"} when its parent renders`,
5624
+ summary: `${childName} gets ${unstable.length} ${unstable.length === 1 ? "prop" : "props"} created during render that can prevent memoization from skipping it`,
5412
5625
  editorRanges: highlights.map((node, index) => ({
5413
5626
  node,
5414
- summary: `${childName} gets a fresh ${unstable[index]} prop each render. Memoization cannot skip it.`
5627
+ summary: `${childName} gets a ${unstable[index]} prop created during render that can prevent memoization from skipping it`
5415
5628
  })),
5416
- help: "Fresh table or function identity prevents the shallow memo comparison from skipping this child. Stabilize the prop only when the parent rerenders frequently, move construction into the child, or remove ineffective memoization."
5629
+ 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
5630
  });
5418
5631
  }
5419
5632
  return diagnostics;
@@ -7587,30 +7800,32 @@ function currentConditionalHookMode(context) {
7587
7800
  }
7588
7801
  return null;
7589
7802
  }
7590
- function controlledParameterIndex(controlFlow, fn, summary) {
7803
+ function controlledParameterIndex(controlFlow, fn, summary, node) {
7591
7804
  if (controlFlow.type !== "if_statement")
7592
7805
  return null;
7593
- const header = (controlFlow.text.split(/\bthen\b/s, 1)[0] ?? "").trim();
7594
- for (const [conditionName, index] of Object.entries(summary.conditionVariables)) {
7595
- const escaped = conditionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7596
- const patterns = [
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
- }
7806
+ const conditions = guardConditions(controlFlow, node);
7807
+ if (conditions.length === 0)
7808
+ return null;
7809
+ const variables = { ...summary.conditionVariables };
7605
7810
  for (const index of summary.parameterIndexes) {
7606
7811
  const name = fn.parameters[index];
7607
- if (!name)
7608
- continue;
7609
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7610
- if (new RegExp(`^if\\s+(?:not\\s+)?${escaped}\\s*$`).test(header))
7611
- return index;
7812
+ if (name)
7813
+ variables[name] ??= index;
7612
7814
  }
7613
- return null;
7815
+ const indexes = conditions.map((condition) => {
7816
+ for (const [name, index] of Object.entries(variables)) {
7817
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7818
+ const patterns = [
7819
+ new RegExp(`^(?:not\\s+)?${escaped}\\s*$`),
7820
+ new RegExp(`^${escaped}\\s*(?:==|~=)\\s*(?:true|false|nil)\\s*$`),
7821
+ new RegExp(`^(?:true|false|nil)\\s*(?:==|~=)\\s*${escaped}\\s*$`)
7822
+ ];
7823
+ if (patterns.some((pattern) => pattern.test(condition.text.trim())))
7824
+ return index;
7825
+ }
7826
+ return null;
7827
+ });
7828
+ return indexes.every((index) => index !== null) ? indexes[0] : null;
7614
7829
  }
7615
7830
  function dynamicHookModeFixPreview(hookName, argumentText) {
7616
7831
  return {
@@ -7812,28 +8027,104 @@ function staticModuleTables(context) {
7812
8027
  }
7813
8028
  return candidates;
7814
8029
  }
7815
- function simpleConditionRoot(controlFlow) {
7816
- if (controlFlow.type !== "if_statement" && controlFlow.type !== "if_expression")
7817
- return null;
8030
+ function containsNode(container, node) {
8031
+ return node.startIndex >= container.startIndex && node.endIndex <= container.endIndex;
8032
+ }
8033
+ function branchContaining(controlFlow, node) {
8034
+ return controlFlow.namedChildren.find((child) => containsNode(child, node)) ?? null;
8035
+ }
8036
+ function guardConditions(controlFlow, node) {
7818
8037
  const condition = controlFlow.namedChildren[0];
7819
8038
  if (!condition)
7820
- return null;
8039
+ return [];
8040
+ if (controlFlow.type !== "if_statement" && controlFlow.type !== "if_expression")
8041
+ return [condition];
8042
+ if (containsNode(condition, node))
8043
+ return [];
8044
+ const conditions = [condition];
8045
+ for (const clause of controlFlow.namedChildren.slice(1)) {
8046
+ if (clause.startIndex > node.startIndex)
8047
+ break;
8048
+ if (clause.type !== "elseif_statement" && clause.type !== "elseif_clause")
8049
+ continue;
8050
+ const clauseCondition = clause.namedChildren[0];
8051
+ if (clauseCondition && !containsNode(clauseCondition, node))
8052
+ conditions.push(clauseCondition);
8053
+ }
8054
+ return conditions;
8055
+ }
8056
+ function conditionallyContains(controlFlow, node) {
8057
+ if (controlFlow.type === "if_statement" || controlFlow.type === "if_expression")
8058
+ return guardConditions(controlFlow, node).length > 0;
8059
+ if (controlFlow.type === "for_statement")
8060
+ return controlFlow.namedChildren.some((child) => child.type === "block" && containsNode(child, node));
8061
+ if (CONDITIONAL_TYPES.has(controlFlow.type))
8062
+ return true;
8063
+ if (controlFlow.type !== "binary_expression" || !controlFlow.children.some((child) => child.type === "and" || child.type === "or"))
8064
+ return false;
8065
+ const right = controlFlow.childForFieldName("right") ?? controlFlow.namedChildren[1];
8066
+ return Boolean(right && containsNode(right, node));
8067
+ }
8068
+ function simpleConditionRoot(condition) {
7821
8069
  const text = condition.text.trim();
7822
8070
  const match = text.match(/^(?:not\s+)?([A-Za-z_][A-Za-z0-9_]*)(?:\s*(?:==|~=)\s*(?:true|false|nil))?$/);
7823
8071
  return match?.[1] ?? null;
7824
8072
  }
7825
- function conditionIsProvablyInvariant(controlFlow, fn, context, moduleInvariants) {
7826
- const condition = controlFlow.namedChildren[0];
7827
- if (!condition)
8073
+ var unstableLocalNamesCache = new WeakMap;
8074
+ function unstableLocalNames(fn, context) {
8075
+ const cached = unstableLocalNamesCache.get(fn);
8076
+ if (cached)
8077
+ return cached;
8078
+ const declared = new Set;
8079
+ const unstable = new Set;
8080
+ for (const node of context.walk(fn.node)) {
8081
+ if (node.type === "variable_declaration" && context.nearestFunction(node) === fn) {
8082
+ for (const name of declarationNames2(node)) {
8083
+ if (declared.has(name))
8084
+ unstable.add(name);
8085
+ declared.add(name);
8086
+ }
8087
+ }
8088
+ if (node.type !== "assignment_statement" || node.parent?.type === "variable_declaration")
8089
+ continue;
8090
+ const variables = node.namedChildren.find((child) => child.type === "variable_list");
8091
+ for (const target of variables?.namedChildren ?? []) {
8092
+ if (target.type === "identifier")
8093
+ unstable.add(target.text);
8094
+ }
8095
+ }
8096
+ unstableLocalNamesCache.set(fn, unstable);
8097
+ return unstable;
8098
+ }
8099
+ function moduleNameIsUnshadowed(name, node, fn, context) {
8100
+ let owner = fn;
8101
+ while (owner) {
8102
+ if (owner.parameters.includes(name) || isBindingShadowedBetween(node, owner, name))
8103
+ return false;
8104
+ owner = context.nearestFunction(owner.node);
8105
+ }
8106
+ return true;
8107
+ }
8108
+ function conditionIsProvablyInvariant(controlFlow, fn, context, moduleInvariants, node = controlFlow) {
8109
+ if (!["if_statement", "if_expression", "binary_expression"].includes(controlFlow.type))
7828
8110
  return false;
7829
- if (["true", "false", "nil", "number", "string"].includes(condition.type))
7830
- return true;
7831
- const root = simpleConditionRoot(controlFlow);
7832
- if (!root)
8111
+ const conditions = guardConditions(controlFlow, node);
8112
+ if (conditions.length === 0)
7833
8113
  return false;
7834
- if (moduleInvariants.has(root))
7835
- return true;
7836
- return Boolean(context.model.stableVariablesByFunction.get(nodeKey(fn.node))?.has(root));
8114
+ return conditions.every((condition) => {
8115
+ if (["true", "false", "nil", "number", "string"].includes(condition.type))
8116
+ return true;
8117
+ const root = simpleConditionRoot(condition);
8118
+ if (!root)
8119
+ return false;
8120
+ if (moduleInvariants.has(root) && moduleNameIsUnshadowed(root, condition, fn, context))
8121
+ return true;
8122
+ if (isNameShadowedBetween(condition, fn, root))
8123
+ return false;
8124
+ if (unstableLocalNames(fn, context).has(root))
8125
+ return false;
8126
+ return Boolean(context.model.stableVariablesByFunction.get(nodeKey(fn.node))?.has(root));
8127
+ });
7837
8128
  }
7838
8129
  function branchHookSequence(branch, fn, context) {
7839
8130
  const hooks = [];
@@ -7848,28 +8139,45 @@ function branchHookSequence(branch, fn, context) {
7848
8139
  const path = context.resolveCallPath(rawPath);
7849
8140
  if (!isHookPath(path))
7850
8141
  continue;
7851
- const nestedControl = findAncestorBetween(node, branch, (ancestor) => CONDITIONAL_TYPES.has(ancestor.type));
7852
- if (nestedControl)
8142
+ const nestedControl = node.id === branch.id ? null : findAncestorBetween(node, branch, (ancestor) => conditionallyContains(ancestor, node));
8143
+ if (nestedControl || conditionallyContains(branch, node))
7853
8144
  return null;
7854
8145
  hooks.push(path);
7855
8146
  }
7856
8147
  return hooks;
7857
8148
  }
7858
8149
  function ifBranchesHaveEquivalentBuiltInTopology(controlFlow, fn, context) {
7859
- if (controlFlow.type !== "if_statement")
8150
+ const isStatement = controlFlow.type === "if_statement";
8151
+ if (!isStatement && controlFlow.type !== "if_expression")
7860
8152
  return false;
7861
- const thenBlock = controlFlow.namedChildren.find((child) => child.type === "block");
7862
- const elseStatement = controlFlow.namedChildren.find((child) => child.type === "else_statement");
7863
- const elseBlock = elseStatement?.namedChildren.find((child) => child.type === "block");
7864
- if (!thenBlock || !elseBlock)
8153
+ const firstBranch = isStatement ? controlFlow.namedChildren.find((child) => child.type === "block") : controlFlow.namedChildren[1];
8154
+ const elseClause = controlFlow.namedChildren.find((child) => child.type === "else_statement" || child.type === "else_clause");
8155
+ const lastBranch = isStatement ? elseClause?.namedChildren.find((child) => child.type === "block") : elseClause?.namedChildren[0];
8156
+ if (!firstBranch || !lastBranch)
7865
8157
  return false;
7866
- const thenHooks = branchHookSequence(thenBlock, fn, context);
7867
- const elseHooks = branchHookSequence(elseBlock, fn, context);
7868
- if (!thenHooks || !elseHooks || thenHooks.length !== elseHooks.length)
8158
+ const branches = [firstBranch];
8159
+ for (const clause of controlFlow.namedChildren) {
8160
+ if (clause.type !== "elseif_statement" && clause.type !== "elseif_clause")
8161
+ continue;
8162
+ const condition = clause.namedChildren[0];
8163
+ if (!condition)
8164
+ return false;
8165
+ const conditionHooks = branchHookSequence(condition, fn, context);
8166
+ if (!conditionHooks || conditionHooks.length > 0)
8167
+ return false;
8168
+ const branch = isStatement ? clause.namedChildren.find((child) => child.type === "block") : clause.namedChildren[1];
8169
+ if (!branch)
8170
+ return false;
8171
+ branches.push(branch);
8172
+ }
8173
+ branches.push(lastBranch);
8174
+ const sequences = branches.map((branch) => branchHookSequence(branch, fn, context));
8175
+ const first = sequences[0];
8176
+ if (!first)
7869
8177
  return false;
7870
- if (!thenHooks.every((path, index) => path === elseHooks[index]))
8178
+ if (!sequences.every((sequence) => sequence && sequence.length === first.length && sequence.every((path, index) => path === first[index])))
7871
8179
  return false;
7872
- return thenHooks.every((path) => path.startsWith("React."));
8180
+ return first.every((path) => path.startsWith("React."));
7873
8181
  }
7874
8182
  function stateModeStability(name, owner, context) {
7875
8183
  const binding = context.model.stateBindings.find((candidate) => candidate.owner === owner && candidate.valueName === name);
@@ -7974,7 +8282,21 @@ function controlledAccessPaths(summary, parameterIndex) {
7974
8282
  paths.add("");
7975
8283
  return [...paths];
7976
8284
  }
7977
- function loopIterationIsProvablyStable(loop, imports, owner, stableShapes, moduleTables) {
8285
+ function loopIterationIsProvablyStable(loop, imports, owner, stableShapes, moduleTables, context, moduleInvariants) {
8286
+ for (const node of context.walk(loop)) {
8287
+ if (!["break_statement", "continue_statement", "return_statement"].includes(node.type))
8288
+ continue;
8289
+ if (context.nearestFunction(node) !== owner)
8290
+ continue;
8291
+ if (node.type !== "return_statement") {
8292
+ const targetLoop = findAncestorBetween(node, owner.node, (ancestor) => ["for_statement", "while_statement", "repeat_statement"].includes(ancestor.type));
8293
+ if (targetLoop?.id !== loop.id)
8294
+ continue;
8295
+ }
8296
+ const dynamicGuard = findAncestorBetween(node, loop, (ancestor) => conditionallyContains(ancestor, node) && !conditionIsProvablyInvariant(ancestor, owner, context, moduleInvariants, node));
8297
+ if (dynamicGuard)
8298
+ return false;
8299
+ }
7978
8300
  const numeric = loop.namedChildren.find((child) => child.type === "for_numeric_clause");
7979
8301
  if (numeric) {
7980
8302
  const text = numeric.text;
@@ -8047,23 +8369,46 @@ function reachableReturnsBefore(call, fn, context, cache) {
8047
8369
  returns = [...context.walk(fn.body)].filter((node) => node.type === "return_statement" && !isNestedInsideFunction(node, fn.node) && !impossibleNilGuardReturn(node, fn, parameterTypes));
8048
8370
  cache.set(key, returns);
8049
8371
  }
8050
- return returns.filter((node) => node.endIndex <= call.startIndex);
8372
+ return returns.filter((node) => {
8373
+ if (node.endIndex > call.startIndex)
8374
+ return false;
8375
+ return !findAncestorBetween(node, fn.node, (ancestor) => {
8376
+ if (ancestor.type !== "if_statement")
8377
+ return false;
8378
+ const returnBranch = branchContaining(ancestor, node);
8379
+ const callBranch = branchContaining(ancestor, call);
8380
+ return Boolean(returnBranch && callBranch && returnBranch.id !== callBranch.id);
8381
+ });
8382
+ });
8051
8383
  }
8052
8384
  function returnIsControlledByStableTopologyMode(node, fn, summary, context, moduleInvariants) {
8053
8385
  let current = node.parent;
8386
+ let sawStableGuard = false;
8054
8387
  while (current && current !== fn.node) {
8055
- if (current.type === "if_statement" || current.type === "if_expression") {
8056
- if (summary && controlledParameterIndex(current, fn, summary) !== null)
8057
- return true;
8058
- if (conditionIsProvablyInvariant(current, fn, context, moduleInvariants))
8059
- return true;
8388
+ if (["for_statement", "while_statement", "repeat_statement"].includes(current.type))
8060
8389
  return false;
8390
+ if (current.type === "if_statement" || current.type === "if_expression") {
8391
+ if (summary && controlledParameterIndex(current, fn, summary, node) !== null)
8392
+ sawStableGuard = true;
8393
+ else if (conditionIsProvablyInvariant(current, fn, context, moduleInvariants, node))
8394
+ sawStableGuard = true;
8395
+ else
8396
+ return false;
8061
8397
  }
8062
8398
  current = current.parent;
8063
8399
  }
8064
- return false;
8400
+ return sawStableGuard;
8065
8401
  }
8066
8402
  function conditionalFixPreview(kind, hookName) {
8403
+ if (kind === "short-circuit expression") {
8404
+ return {
8405
+ kind: "pattern",
8406
+ before: `local result = enabled and renderChild(${hookName}())`,
8407
+ after: `local value = ${hookName}()
8408
+ local result = enabled and renderChild(value)`,
8409
+ note: "Call the hook before the short-circuit expression, then branch on its result"
8410
+ };
8411
+ }
8067
8412
  if (hookName === "React.useEffect" && kind === "if statement") {
8068
8413
  return {
8069
8414
  kind: "pattern",
@@ -8142,6 +8487,34 @@ end`,
8142
8487
  note: "Call hooks before any reachable early return so every render executes the same hook sequence."
8143
8488
  };
8144
8489
  }
8490
+ function callbackFunctions(context) {
8491
+ const callbacks = new Set;
8492
+ const addCallback = (expression) => {
8493
+ const fn = resolveLocalFunction(context, expression);
8494
+ if (fn)
8495
+ callbacks.add(fn.node.id);
8496
+ };
8497
+ for (const call of context.findCalls()) {
8498
+ const path = reactApiPath(context, call);
8499
+ const args = context.callArguments(call);
8500
+ for (const [index, argument] of args.entries()) {
8501
+ if (index === 0 && ["React.memo", "React.forwardRef", "React.createElement"].includes(path ?? ""))
8502
+ continue;
8503
+ if (path?.startsWith("React.use") || path === "React.memo" || unwrapExpression(argument).type === "function_definition")
8504
+ addCallback(argument);
8505
+ }
8506
+ if (path !== "React.createElement" || args[0]?.type !== "string" || args[1]?.type !== "table_constructor")
8507
+ continue;
8508
+ for (const field of args[1].namedChildren) {
8509
+ if (field.type !== "field")
8510
+ continue;
8511
+ const value = field.namedChildren.at(-1);
8512
+ if (value)
8513
+ addCallback(value);
8514
+ }
8515
+ }
8516
+ return callbacks;
8517
+ }
8145
8518
  var rulesOfHooks = {
8146
8519
  id: "react-luau/rules-of-hooks",
8147
8520
  category: "Hooks",
@@ -8162,6 +8535,7 @@ var rulesOfHooks = {
8162
8535
  const modeImports = conditionalHookModeImports(context);
8163
8536
  const ignoredHookImports = nonReactHookImports(context);
8164
8537
  const currentModeSummary = currentConditionalHookMode(context);
8538
+ const callbacks = callbackFunctions(context);
8165
8539
  const reachableReturns = new Map;
8166
8540
  for (const call of context.findCalls()) {
8167
8541
  const rawPath = context.getCallPath(call);
@@ -8173,11 +8547,11 @@ var rulesOfHooks = {
8173
8547
  if (!isHookPath(path))
8174
8548
  continue;
8175
8549
  const fn = context.nearestFunction(call);
8176
- if (!fn || !fn.isComponent && !fn.isHook) {
8550
+ if (!fn || !fn.isComponent && !fn.isHook || callbacks.has(fn.node.id)) {
8177
8551
  diagnostics.push({
8178
8552
  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.",
8553
+ message: `Hook ${path} is called outside a React component or custom hook`,
8554
+ help: "Move the hook into a component or a custom hook whose name starts with use",
8181
8555
  fixPreview: {
8182
8556
  kind: "pattern",
8183
8557
  before: `local value = ${path}()
@@ -8189,7 +8563,7 @@ end`,
8189
8563
  local value = ${path}()
8190
8564
  return value
8191
8565
  end`,
8192
- note: "Hooks need a React-owned component or custom-hook call stack."
8566
+ note: "Hooks need a React-owned component or custom-hook call stack"
8193
8567
  }
8194
8568
  });
8195
8569
  continue;
@@ -8220,24 +8594,28 @@ end`,
8220
8594
  });
8221
8595
  }
8222
8596
  }
8223
- const controlFlow = findAncestorBetween(call, fn.node, (node) => CONDITIONAL_TYPES.has(node.type));
8597
+ const controlFlow = findAncestorBetween(call, fn.node, (node) => {
8598
+ if (!conditionallyContains(node, call))
8599
+ return false;
8600
+ if (currentModeSummary && controlledParameterIndex(node, fn, currentModeSummary, call) !== null)
8601
+ return false;
8602
+ if (conditionIsProvablyInvariant(node, fn, context, moduleInvariants, call))
8603
+ return false;
8604
+ if (ifBranchesHaveEquivalentBuiltInTopology(node, fn, context))
8605
+ return false;
8606
+ if (node.type === "for_statement" && loopIterationIsProvablyStable(node, staticImports, fn, stableShapes, moduleTables, context, moduleInvariants))
8607
+ return false;
8608
+ return true;
8609
+ });
8224
8610
  if (controlFlow) {
8225
- if (currentModeSummary && controlledParameterIndex(controlFlow, fn, currentModeSummary) !== null)
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("_", " ");
8611
+ const kind = controlFlow.type === "binary_expression" ? "short-circuit expression" : controlFlow.type.replaceAll("_", " ");
8234
8612
  const loopLike = controlFlow.type === "for_statement" || controlFlow.type === "while_statement" || controlFlow.type === "repeat_statement";
8235
8613
  diagnostics.push({
8236
8614
  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.` : `Hook ${path} is called inside ${kind}, so the hook may be skipped on some renders.`,
8238
- summary: loopLike ? `${path} may run a different number of times between renders.` : `${path} may be skipped on some renders.`,
8239
- explanation: loopLike ? `This ${kind} may change how many times ${path} is called.` : `This ${kind} can skip ${path} on some renders.`,
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." : "Call the hook unconditionally at the top level, then branch on the returned value.",
8615
+ 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`,
8616
+ summary: loopLike ? `${path} may run a different number of times between renders` : `${path} may be skipped on some renders`,
8617
+ explanation: loopLike ? `This ${kind} may change how many times ${path} is called` : `This ${kind} can skip ${path} on some renders`,
8618
+ 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
8619
  fixPreview: conditionalFixPreview(kind, path)
8242
8620
  });
8243
8621
  continue;
@@ -8610,7 +8988,7 @@ var unstableContextValue = {
8610
8988
  id: "react-luau/unstable-context-value",
8611
8989
  category: "Performance",
8612
8990
  severity: "warning",
8613
- description: "Avoid recreating context value tables on every provider render when identity matters.",
8991
+ description: "Avoid recreating context reference values during render when identity matters",
8614
8992
  run(context) {
8615
8993
  const diagnostics = [];
8616
8994
  for (const call of context.findCalls()) {
@@ -8625,13 +9003,13 @@ var unstableContextValue = {
8625
9003
  const props = args[1];
8626
9004
  if (!/\.Provider$/.test(element) || !props || props.type !== "table_constructor")
8627
9005
  continue;
8628
- const valueField = props.namedChildren.find((field) => field.type === "field" && fieldName(field) === "value" && fieldValue(field)?.type === "table_constructor");
9006
+ const valueField = props.namedChildren.find((field) => field.type === "field" && fieldName(field) === "value" && freshValueKind(context, fieldValue(field), component));
8629
9007
  if (!valueField)
8630
9008
  continue;
8631
9009
  diagnostics.push({
8632
9010
  node: fieldNameNode(valueField),
8633
- message: "Context Provider receives a new inline value table on every render.",
8634
- help: "Memoize the provider value from its real dependencies when consumer updates depend on object identity."
9011
+ message: "Context Provider can receive a new reference value when the component renders",
9012
+ help: "Memoize the provider value from its real dependencies when consumer updates depend on object identity"
8635
9013
  });
8636
9014
  }
8637
9015
  return diagnostics;
@@ -8960,6 +9338,7 @@ async function analyzeReactFile(input, tree, selectedRules = rules) {
8960
9338
  project: input.project
8961
9339
  };
8962
9340
  const context = createRuleContext(file);
9341
+ const syntaxFindings = parsedTree.rootNode.hasError ? parseErrors.run(context) : [];
8963
9342
  const categorySet = input.categories && input.categories.length > 0 ? new Set(input.categories) : null;
8964
9343
  const isSuppressed = input.respectInlineDisables ? createInlineSuppressionChecker(input.source) : () => false;
8965
9344
  const diagnostics = [];
@@ -8971,7 +9350,7 @@ async function analyzeReactFile(input, tree, selectedRules = rules) {
8971
9350
  const severity = effectiveSeverity(rule.severity, rule.id, input.config);
8972
9351
  if (!severity)
8973
9352
  continue;
8974
- const findings = rule.run(context);
9353
+ const findings = rule.id === parseErrors.id ? syntaxFindings : rule.run(context);
8975
9354
  for (const finding of findings) {
8976
9355
  const configuredSeverity = input.config.rules?.[rule.id];
8977
9356
  const diagnostic = toDiagnostic(file, rule.id, rule.category, severity, {
@@ -8992,7 +9371,8 @@ async function analyzeReactFile(input, tree, selectedRules = rules) {
8992
9371
  relativePath: input.relativePath,
8993
9372
  isReactFile: model.isReactFile,
8994
9373
  scanned: true,
8995
- diagnostics: filteredDiagnostics
9374
+ diagnostics: filteredDiagnostics,
9375
+ unverified: syntaxFindings.length > 0
8996
9376
  };
8997
9377
  } finally {
8998
9378
  if (!tree)
@@ -9979,5 +10359,5 @@ parentPort.on("message", async (request) => {
9979
10359
  }
9980
10360
  });
9981
10361
 
9982
- //# debugId=420377AA4E8B7A3664756E2164756E21
10362
+ //# debugId=3E81A30ECFF40BE164756E2164756E21
9983
10363
  //# sourceMappingURL=scan-worker.js.map