oxlint-plugin-react-doctor 0.7.6-dev.778f1a2 → 0.7.6-dev.81e6647

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.
Files changed (2) hide show
  1. package/dist/index.js +247 -45
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6296,6 +6296,131 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
6296
6296
  return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
6297
6297
  };
6298
6298
  //#endregion
6299
+ //#region src/plugin/utils/collect-safely-validated-local-storage-keys.ts
6300
+ const isLocalStorageCall = (node, methodName) => {
6301
+ if (!isNodeOfType(node, "CallExpression")) return false;
6302
+ const callee = stripParenExpression(node.callee);
6303
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
6304
+ const receiver = stripParenExpression(callee.object);
6305
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "localStorage" && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
6306
+ };
6307
+ const catchReturnsFallback = (tryStatement) => {
6308
+ const handler = tryStatement.handler;
6309
+ if (!handler) return false;
6310
+ let hasReturn = false;
6311
+ let hasThrow = false;
6312
+ walkAst(handler.body, (child) => {
6313
+ if (isFunctionLike$1(child)) return false;
6314
+ if (isNodeOfType(child, "ReturnStatement")) hasReturn = true;
6315
+ if (isNodeOfType(child, "ThrowStatement")) hasThrow = true;
6316
+ });
6317
+ return hasReturn && !hasThrow;
6318
+ };
6319
+ const resolveValidatorFunction = (callee, scopes) => {
6320
+ const unwrappedCallee = stripParenExpression(callee);
6321
+ if (!isNodeOfType(unwrappedCallee, "Identifier")) return null;
6322
+ const symbol = scopes.symbolFor(unwrappedCallee);
6323
+ if (!symbol) return null;
6324
+ const candidate = symbol.kind === "function" ? symbol.declarationNode : symbol.initializer;
6325
+ return isFunctionLike$1(candidate) ? candidate : null;
6326
+ };
6327
+ const validatorChecksPayloadProperties = (validatorFunction, scopes) => {
6328
+ if (!isFunctionLike$1(validatorFunction)) return false;
6329
+ const firstParameter = validatorFunction.params?.[0];
6330
+ if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
6331
+ const parameterSymbol = scopes.symbolFor(firstParameter);
6332
+ if (!parameterSymbol) return false;
6333
+ const payloadSymbolIds = new Set([parameterSymbol.id]);
6334
+ let hasPropertyTypeCheck = false;
6335
+ walkAst(validatorFunction.body, (child) => {
6336
+ if (isFunctionLike$1(child)) return false;
6337
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier") && child.init) {
6338
+ const initializer = stripParenExpression(child.init);
6339
+ if (isNodeOfType(initializer, "Identifier")) {
6340
+ const initializerSymbol = scopes.symbolFor(initializer);
6341
+ if (initializerSymbol && payloadSymbolIds.has(initializerSymbol.id)) {
6342
+ const aliasSymbol = scopes.symbolFor(child.id);
6343
+ if (aliasSymbol) payloadSymbolIds.add(aliasSymbol.id);
6344
+ }
6345
+ }
6346
+ }
6347
+ if (!isNodeOfType(child, "UnaryExpression") || child.operator !== "typeof") return;
6348
+ const checkedValue = stripParenExpression(child.argument);
6349
+ if (!isNodeOfType(checkedValue, "MemberExpression")) return;
6350
+ const receiver = stripParenExpression(checkedValue.object);
6351
+ if (!isNodeOfType(receiver, "Identifier")) return;
6352
+ const receiverSymbol = scopes.symbolFor(receiver);
6353
+ if (receiverSymbol && payloadSymbolIds.has(receiverSymbol.id)) hasPropertyTypeCheck = true;
6354
+ });
6355
+ return hasPropertyTypeCheck;
6356
+ };
6357
+ const incrementCount = (counts, keyValue) => {
6358
+ counts.set(keyValue, (counts.get(keyValue) ?? 0) + 1);
6359
+ };
6360
+ const isReturnedExpression = (expression) => {
6361
+ const parent = expression.parent;
6362
+ return Boolean(parent && isNodeOfType(parent, "ReturnStatement") && parent.argument === expression);
6363
+ };
6364
+ const collectSafelyValidatedLocalStorageKeys = ({ programNode, scopes, resolveKey }) => {
6365
+ const readCountsByKey = /* @__PURE__ */ new Map();
6366
+ const safeReadCountsByKey = /* @__PURE__ */ new Map();
6367
+ const safeReadSymbolIds = /* @__PURE__ */ new Set();
6368
+ walkAst(programNode, (child) => {
6369
+ if (!isNodeOfType(child, "CallExpression") || !isLocalStorageCall(child, "getItem")) return;
6370
+ const keyArgument = child.arguments?.[0];
6371
+ if (!keyArgument) return;
6372
+ const keyValue = resolveKey(keyArgument);
6373
+ if (keyValue !== null) incrementCount(readCountsByKey, keyValue);
6374
+ });
6375
+ walkAst(programNode, (child) => {
6376
+ if (!isNodeOfType(child, "TryStatement") || !catchReturnsFallback(child)) return;
6377
+ const rawValueKeys = /* @__PURE__ */ new Map();
6378
+ const parsedValueSources = /* @__PURE__ */ new Map();
6379
+ walkAst(child.block, (tryChild) => {
6380
+ if (isFunctionLike$1(tryChild)) return false;
6381
+ if (isNodeOfType(tryChild, "VariableDeclarator") && isNodeOfType(tryChild.id, "Identifier") && tryChild.init) {
6382
+ const initializer = stripParenExpression(tryChild.init);
6383
+ if (isNodeOfType(initializer, "CallExpression") && isLocalStorageCall(initializer, "getItem")) {
6384
+ const keyArgument = initializer.arguments?.[0];
6385
+ const bindingSymbol = scopes.symbolFor(tryChild.id);
6386
+ if (keyArgument && bindingSymbol) {
6387
+ const keyValue = resolveKey(keyArgument);
6388
+ if (keyValue !== null) rawValueKeys.set(bindingSymbol.id, keyValue);
6389
+ }
6390
+ }
6391
+ if (isNodeOfType(initializer, "CallExpression") && isGlobalMethodCall(initializer, "JSON", "parse")) {
6392
+ const rawValueArgument = initializer.arguments?.[0];
6393
+ const parsedValueSymbol = scopes.symbolFor(tryChild.id);
6394
+ if (rawValueArgument && isNodeOfType(rawValueArgument, "Identifier") && parsedValueSymbol) {
6395
+ const rawValueSymbol = scopes.symbolFor(rawValueArgument);
6396
+ if (rawValueSymbol && rawValueKeys.has(rawValueSymbol.id)) parsedValueSources.set(parsedValueSymbol.id, rawValueSymbol.id);
6397
+ }
6398
+ }
6399
+ }
6400
+ if (!isNodeOfType(tryChild, "ConditionalExpression") || !isReturnedExpression(tryChild)) return;
6401
+ const test = stripParenExpression(tryChild.test);
6402
+ if (!isNodeOfType(test, "CallExpression")) return;
6403
+ const testedValue = test.arguments?.[0];
6404
+ if (!testedValue || !isNodeOfType(testedValue, "Identifier")) return;
6405
+ const parsedValueSymbol = scopes.symbolFor(testedValue);
6406
+ if (!parsedValueSymbol) return;
6407
+ const rawValueSymbolId = parsedValueSources.get(parsedValueSymbol.id);
6408
+ if (rawValueSymbolId === void 0) return;
6409
+ const returnedValue = stripParenExpression(tryChild.consequent);
6410
+ if (!isNodeOfType(returnedValue, "Identifier") || scopes.symbolFor(returnedValue)?.id !== parsedValueSymbol.id) return;
6411
+ const validatorFunction = resolveValidatorFunction(test.callee, scopes);
6412
+ if (!validatorFunction || !validatorChecksPayloadProperties(validatorFunction, scopes)) return;
6413
+ const keyValue = rawValueKeys.get(rawValueSymbolId);
6414
+ if (keyValue === void 0 || safeReadSymbolIds.has(rawValueSymbolId)) return;
6415
+ safeReadSymbolIds.add(rawValueSymbolId);
6416
+ incrementCount(safeReadCountsByKey, keyValue);
6417
+ });
6418
+ });
6419
+ const safeKeys = /* @__PURE__ */ new Set();
6420
+ for (const [keyValue, readCount] of readCountsByKey) if (safeReadCountsByKey.get(keyValue) === readCount) safeKeys.add(keyValue);
6421
+ return safeKeys;
6422
+ };
6423
+ //#endregion
6299
6424
  //#region src/plugin/rules/client/client-localstorage-no-version.ts
6300
6425
  const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
6301
6426
  const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
@@ -6317,26 +6442,39 @@ const clientLocalstorageNoVersion = defineRule({
6317
6442
  severity: "warn",
6318
6443
  category: "Correctness",
6319
6444
  recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
6320
- create: (context) => ({ CallExpression(node) {
6321
- if (!isNodeOfType(node.callee, "MemberExpression")) return;
6322
- const receiver = stripParenExpression(node.callee.object);
6323
- if (!isNodeOfType(receiver, "Identifier")) return;
6324
- if (receiver.name !== "localStorage") return;
6325
- if (!isNodeOfType(node.callee.property, "Identifier")) return;
6326
- if (node.callee.property.name !== "setItem") return;
6327
- const keyArg = node.arguments?.[0];
6328
- if (!keyArg) return;
6329
- const keyValue = resolveStringKey(keyArg, context);
6330
- if (keyValue === null) return;
6331
- if (isVersionedKey(keyValue)) return;
6332
- const valueArg = node.arguments?.[1];
6333
- if (!valueArg) return;
6334
- if (!isJsonStringifyCall(valueArg)) return;
6335
- context.report({
6336
- node: keyArg,
6337
- message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
6338
- });
6339
- } })
6445
+ create: (context) => {
6446
+ let safelyValidatedStorageKeys = /* @__PURE__ */ new Set();
6447
+ return {
6448
+ Program(node) {
6449
+ safelyValidatedStorageKeys = collectSafelyValidatedLocalStorageKeys({
6450
+ programNode: node,
6451
+ scopes: context.scopes,
6452
+ resolveKey: (keyNode) => resolveStringKey(keyNode, context)
6453
+ });
6454
+ },
6455
+ CallExpression(node) {
6456
+ if (!isNodeOfType(node.callee, "MemberExpression")) return;
6457
+ const receiver = stripParenExpression(node.callee.object);
6458
+ if (!isNodeOfType(receiver, "Identifier")) return;
6459
+ if (receiver.name !== "localStorage") return;
6460
+ if (!isNodeOfType(node.callee.property, "Identifier")) return;
6461
+ if (node.callee.property.name !== "setItem") return;
6462
+ const keyArg = node.arguments?.[0];
6463
+ if (!keyArg) return;
6464
+ const keyValue = resolveStringKey(keyArg, context);
6465
+ if (keyValue === null) return;
6466
+ if (isVersionedKey(keyValue)) return;
6467
+ if (safelyValidatedStorageKeys.has(keyValue)) return;
6468
+ const valueArg = node.arguments?.[1];
6469
+ if (!valueArg) return;
6470
+ if (!isJsonStringifyCall(valueArg)) return;
6471
+ context.report({
6472
+ node: keyArg,
6473
+ message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
6474
+ });
6475
+ }
6476
+ };
6477
+ }
6340
6478
  });
6341
6479
  //#endregion
6342
6480
  //#region src/plugin/rules/client/client-passive-event-listeners.ts
@@ -20395,8 +20533,24 @@ const hasDirective = (programNode, directive) => {
20395
20533
  return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
20396
20534
  };
20397
20535
  //#endregion
20398
- //#region src/plugin/utils/is-component-assignment.ts
20399
- const isComponentAssignment = (node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && isUppercaseName(node.id.name) && Boolean(node.init) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
20536
+ //#region src/plugin/utils/unwrap-object-integrity-expression.ts
20537
+ const OBJECT_INTEGRITY_METHOD_NAMES = new Set([
20538
+ "freeze",
20539
+ "seal",
20540
+ "preventExtensions"
20541
+ ]);
20542
+ const OBJECT_FREEZE_OR_SEAL_METHOD_NAMES = new Set(["freeze", "seal"]);
20543
+ const unwrapObjectIntegrityExpression = (node, scopes, methodNames = OBJECT_INTEGRITY_METHOD_NAMES) => {
20544
+ let expression = stripParenExpression(node);
20545
+ while (isNodeOfType(expression, "CallExpression")) {
20546
+ const callee = stripParenExpression(expression.callee);
20547
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Object" || !scopes.isGlobalReference(callee.object) || !isNodeOfType(callee.property, "Identifier") || !methodNames.has(callee.property.name)) break;
20548
+ const wrappedExpression = expression.arguments[0];
20549
+ if (!wrappedExpression || isNodeOfType(wrappedExpression, "SpreadElement")) break;
20550
+ expression = stripParenExpression(wrappedExpression);
20551
+ }
20552
+ return expression;
20553
+ };
20400
20554
  //#endregion
20401
20555
  //#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
20402
20556
  const nextjsAsyncClientComponent = defineRule({
@@ -20422,10 +20576,10 @@ const nextjsAsyncClientComponent = defineRule({
20422
20576
  },
20423
20577
  VariableDeclarator(node) {
20424
20578
  if (!fileHasUseClient) return;
20425
- if (!isComponentAssignment(node)) return;
20426
- if (!isInlineFunctionExpression(node.init)) return;
20427
- if (!node.init.async) return;
20428
20579
  if (!isNodeOfType(node.id, "Identifier")) return;
20580
+ if (!isUppercaseName(node.id.name) || !node.init) return;
20581
+ const componentFunction = unwrapObjectIntegrityExpression(node.init, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES);
20582
+ if (!isInlineFunctionExpression(componentFunction) || !componentFunction.async) return;
20429
20583
  context.report({
20430
20584
  node,
20431
20585
  message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
@@ -26161,6 +26315,9 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
26161
26315
  return hasRenderOutput;
26162
26316
  };
26163
26317
  //#endregion
26318
+ //#region src/plugin/utils/is-component-assignment.ts
26319
+ const isComponentAssignment = (node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && isUppercaseName(node.id.name) && Boolean(node.init) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
26320
+ //#endregion
26164
26321
  //#region src/plugin/utils/is-component-declaration.ts
26165
26322
  const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
26166
26323
  //#endregion
@@ -32210,11 +32367,12 @@ const isMemoCall = (node) => {
32210
32367
  };
32211
32368
  const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
32212
32369
  const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
32213
- const isInlineReference = (node) => {
32214
- if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "bind") return "functions";
32215
- if (isNodeOfType(node, "ObjectExpression")) return "objects";
32216
- if (isNodeOfType(node, "ArrayExpression")) return "Arrays";
32217
- if (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")) return "JSX";
32370
+ const isInlineReference = (node, scopes) => {
32371
+ const referenceNode = unwrapObjectIntegrityExpression(node, scopes);
32372
+ if (isNodeOfType(referenceNode, "ArrowFunctionExpression") || isNodeOfType(referenceNode, "FunctionExpression") || isNodeOfType(referenceNode, "CallExpression") && isNodeOfType(referenceNode.callee, "MemberExpression") && isNodeOfType(referenceNode.callee.property, "Identifier") && referenceNode.callee.property.name === "bind") return "functions";
32373
+ if (isNodeOfType(referenceNode, "ObjectExpression")) return "objects";
32374
+ if (isNodeOfType(referenceNode, "ArrayExpression")) return "Arrays";
32375
+ if (isNodeOfType(referenceNode, "JSXElement") || isNodeOfType(referenceNode, "JSXFragment")) return "JSX";
32218
32376
  return null;
32219
32377
  };
32220
32378
  const noInlinePropOnMemoComponent = defineRule({
@@ -32244,7 +32402,7 @@ const noInlinePropOnMemoComponent = defineRule({
32244
32402
  let elementName = null;
32245
32403
  if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
32246
32404
  if (!elementName || !memoizedComponentNames.has(elementName)) return;
32247
- const propType = isInlineReference(node.value.expression);
32405
+ const propType = isInlineReference(node.value.expression, context.scopes);
32248
32406
  if (propType) context.report({
32249
32407
  node: node.value.expression,
32250
32408
  message: `This redraws ${elementName} on every render because the prop is ${propType} built right here, so memo() can't skip it. Move it to a stable value with useMemo, useCallback, or module scope`
@@ -43692,13 +43850,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
43692
43850
  }
43693
43851
  return null;
43694
43852
  };
43695
- const resolveHookNameFromInitializer = (initializer) => {
43853
+ const resolveHookNameFromInitializer = (initializer, scopes) => {
43696
43854
  if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
43697
43855
  if (!isNodeOfType(initializer, "Identifier")) return null;
43698
- const binding = findVariableInitializer(initializer, initializer.name);
43699
- if (!binding?.initializer || !isConstDeclaredBinding(binding)) return null;
43700
- if (!isNodeOfType(binding.initializer, "CallExpression")) return null;
43701
- return resolveTanstackQueryHookName(binding.initializer);
43856
+ const resolvedSymbol = resolveConstIdentifierAlias(initializer, scopes);
43857
+ if (resolvedSymbol?.kind !== "const" || !resolvedSymbol.initializer) return null;
43858
+ if (!isNodeOfType(resolvedSymbol.initializer, "CallExpression")) return null;
43859
+ return resolveTanstackQueryHookName(resolvedSymbol.initializer);
43702
43860
  };
43703
43861
  const queryNoRestDestructuring = defineRule({
43704
43862
  id: "query-no-rest-destructuring",
@@ -43711,7 +43869,7 @@ const queryNoRestDestructuring = defineRule({
43711
43869
  if (!isNodeOfType(node.id, "ObjectPattern")) return;
43712
43870
  if (!node.init) return;
43713
43871
  if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
43714
- const hookName = resolveHookNameFromInitializer(node.init);
43872
+ const hookName = resolveHookNameFromInitializer(node.init, context.scopes);
43715
43873
  if (!hookName) return;
43716
43874
  context.report({
43717
43875
  node: node.id,
@@ -45066,7 +45224,7 @@ const DEFERRABLE_HOOK_NAMES = new Set([
45066
45224
  "useParams",
45067
45225
  "usePathname"
45068
45226
  ]);
45069
- const findHookCallBindings = (componentBody) => {
45227
+ const findHookCallBindings = (componentBody, scopes) => {
45070
45228
  const bindings = [];
45071
45229
  if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
45072
45230
  for (const statement of componentBody.body ?? []) {
@@ -45077,8 +45235,10 @@ const findHookCallBindings = (componentBody) => {
45077
45235
  const callee = declarator.init.callee;
45078
45236
  if (!isNodeOfType(callee, "Identifier")) continue;
45079
45237
  if (!DEFERRABLE_HOOK_NAMES.has(callee.name)) continue;
45238
+ const valueSymbol = scopes.symbolFor(declarator.id);
45239
+ if (!valueSymbol) continue;
45080
45240
  bindings.push({
45081
- valueName: declarator.id.name,
45241
+ valueSymbol,
45082
45242
  hookName: callee.name,
45083
45243
  declarator
45084
45244
  });
@@ -45086,6 +45246,49 @@ const findHookCallBindings = (componentBody) => {
45086
45246
  }
45087
45247
  return bindings;
45088
45248
  };
45249
+ const collectExactAliasSymbols = (componentBody, sourceSymbol, scopes) => {
45250
+ const symbols = [sourceSymbol];
45251
+ const symbolIds = new Set([sourceSymbol.id]);
45252
+ const aliasSourceIdentifiers = /* @__PURE__ */ new Set();
45253
+ if (!isNodeOfType(componentBody, "BlockStatement")) return {
45254
+ symbols,
45255
+ aliasSourceIdentifiers
45256
+ };
45257
+ let didFindAlias = true;
45258
+ while (didFindAlias) {
45259
+ didFindAlias = false;
45260
+ for (const statement of componentBody.body ?? []) {
45261
+ if (!isNodeOfType(statement, "VariableDeclaration") || statement.kind !== "const") continue;
45262
+ for (const declarator of statement.declarations ?? []) {
45263
+ let aliasIdentifier = null;
45264
+ let sourceIdentifier = null;
45265
+ const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
45266
+ const arrayBinding = isNodeOfType(declarator.id, "ArrayPattern") ? declarator.id.elements[0] : null;
45267
+ const arrayValueNode = isNodeOfType(initializer, "ArrayExpression") ? initializer.elements[0] : null;
45268
+ const arrayValue = arrayValueNode && !isNodeOfType(arrayValueNode, "SpreadElement") ? stripParenExpression(arrayValueNode) : null;
45269
+ if (isNodeOfType(declarator.id, "Identifier") && isNodeOfType(initializer, "Identifier")) {
45270
+ aliasIdentifier = declarator.id;
45271
+ sourceIdentifier = initializer;
45272
+ } else if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements.length === 1 && isNodeOfType(arrayBinding, "Identifier") && isNodeOfType(initializer, "ArrayExpression") && initializer.elements.length === 1 && isNodeOfType(arrayValue, "Identifier")) {
45273
+ aliasIdentifier = arrayBinding;
45274
+ sourceIdentifier = arrayValue;
45275
+ }
45276
+ if (!aliasIdentifier || !sourceIdentifier) continue;
45277
+ const referencedSymbol = scopes.symbolFor(sourceIdentifier);
45278
+ const aliasSymbol = scopes.symbolFor(aliasIdentifier);
45279
+ if (!referencedSymbol || !symbolIds.has(referencedSymbol.id) || !aliasSymbol || aliasSymbol.kind !== "const" || symbolIds.has(aliasSymbol.id)) continue;
45280
+ symbolIds.add(aliasSymbol.id);
45281
+ symbols.push(aliasSymbol);
45282
+ aliasSourceIdentifiers.add(sourceIdentifier);
45283
+ didFindAlias = true;
45284
+ }
45285
+ }
45286
+ }
45287
+ return {
45288
+ symbols,
45289
+ aliasSourceIdentifiers
45290
+ };
45291
+ };
45089
45292
  const rerenderDeferReadsHook = defineRule({
45090
45293
  id: "rerender-defer-reads-hook",
45091
45294
  title: "URL hook value only read in handlers",
@@ -45096,15 +45299,13 @@ const rerenderDeferReadsHook = defineRule({
45096
45299
  create: (context) => {
45097
45300
  const checkComponent = (componentBody) => {
45098
45301
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
45099
- const bindings = findHookCallBindings(componentBody);
45302
+ const bindings = findHookCallBindings(componentBody, context.scopes);
45100
45303
  if (bindings.length === 0) return;
45101
45304
  const handlerBindingNames = collectHandlerBindingNames(componentBody);
45102
45305
  for (const binding of bindings) {
45306
+ const { symbols, aliasSourceIdentifiers } = collectExactAliasSymbols(componentBody, binding.valueSymbol, context.scopes);
45103
45307
  const referenceLocations = [];
45104
- walkAst(componentBody, (child) => {
45105
- if (isNodeOfType(binding.declarator, "VariableDeclarator") && child === binding.declarator.id) return;
45106
- if (isNodeOfType(child, "Identifier") && child.name === binding.valueName) referenceLocations.push(child);
45107
- });
45308
+ for (const symbol of symbols) for (const reference of symbol.references) if (!aliasSourceIdentifiers.has(reference.identifier)) referenceLocations.push(reference.identifier);
45108
45309
  if (referenceLocations.length === 0) continue;
45109
45310
  if (!referenceLocations.every((ref) => isInsideEventHandler(ref, handlerBindingNames))) continue;
45110
45311
  context.report({
@@ -53053,7 +53254,8 @@ const serverCacheWithObjectLiteral = defineRule({
53053
53254
  if (!isNodeOfType(node.callee, "Identifier")) return;
53054
53255
  if (!cachedFunctionNames.has(node.callee.name)) return;
53055
53256
  const firstArg = node.arguments?.[0];
53056
- if (!isNodeOfType(firstArg, "ObjectExpression")) return;
53257
+ if (!firstArg || isNodeOfType(firstArg, "SpreadElement")) return;
53258
+ if (!isNodeOfType(unwrapObjectIntegrityExpression(firstArg, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES), "ObjectExpression")) return;
53057
53259
  context.report({
53058
53260
  node,
53059
53261
  message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.6-dev.778f1a2",
3
+ "version": "0.7.6-dev.81e6647",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",