oxlint-plugin-react-doctor 0.7.6-dev.8c9a6c4 → 0.7.6-dev.a8cd24e

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 +234 -41
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6021,7 +6021,7 @@ const isPureEventBlockerHandler = (attribute) => {
6021
6021
  //#endregion
6022
6022
  //#region src/plugin/utils/resolve-const-identifier-alias.ts
6023
6023
  const resolveConstIdentifierAlias = (identifier, scopes) => {
6024
- if (!isNodeOfType(identifier, "Identifier")) return null;
6024
+ if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
6025
6025
  const visitedSymbolIds = /* @__PURE__ */ new Set();
6026
6026
  let symbol = scopes.symbolFor(identifier);
6027
6027
  while (symbol?.kind === "const") {
@@ -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
@@ -15972,14 +16110,21 @@ const jsxFilenameExtension = defineRule({
15972
16110
  });
15973
16111
  //#endregion
15974
16112
  //#region src/plugin/utils/is-jsx-fragment-element.ts
15975
- const isJsxFragmentElement = (node) => {
16113
+ const isJsxFragmentElement = (node, scopes) => {
15976
16114
  if (!isNodeOfType(node, "JSXOpeningElement")) return false;
15977
16115
  const elementName = node.name;
15978
- if (isNodeOfType(elementName, "JSXIdentifier")) return elementName.name === "Fragment";
16116
+ if (isNodeOfType(elementName, "JSXIdentifier")) {
16117
+ if (!scopes) return elementName.name === "Fragment";
16118
+ const symbol = resolveConstIdentifierAlias(elementName, scopes);
16119
+ if (!symbol) return elementName.name === "Fragment" && scopes.isGlobalReference(elementName);
16120
+ return isImportedFromReact(symbol) && getImportedName(symbol.declarationNode) === "Fragment";
16121
+ }
15979
16122
  if (isNodeOfType(elementName, "JSXMemberExpression")) {
15980
16123
  if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
15981
- if (elementName.object.name !== "React") return false;
15982
- return elementName.property.name === "Fragment";
16124
+ if (elementName.property.name !== "Fragment") return false;
16125
+ if (!scopes) return elementName.object.name === "React";
16126
+ if (isReactNamespaceImport(elementName.object, scopes)) return true;
16127
+ return elementName.object.name === "React" && scopes.isGlobalReference(elementName.object);
15983
16128
  }
15984
16129
  return false;
15985
16130
  };
@@ -16005,7 +16150,7 @@ const jsxFragments = defineRule({
16005
16150
  if (mode !== "syntax") return;
16006
16151
  if (!node.closingElement) return;
16007
16152
  const openingElement = node.openingElement;
16008
- if (!isJsxFragmentElement(openingElement)) return;
16153
+ if (!isJsxFragmentElement(openingElement, context.scopes)) return;
16009
16154
  if (openingElement.attributes.length > 0) return;
16010
16155
  context.report({
16011
16156
  node: openingElement,
@@ -19146,7 +19291,7 @@ const jsxNoUselessFragment = defineRule({
19146
19291
  return {
19147
19292
  JSXElement(node) {
19148
19293
  const openingElement = node.openingElement;
19149
- if (!isJsxFragmentElement(openingElement)) return;
19294
+ if (!isJsxFragmentElement(openingElement, context.scopes)) return;
19150
19295
  if (hasJsxKeyAttribute(openingElement)) return;
19151
19296
  if (checkChildren(node, openingElement, node.children)) return;
19152
19297
  if (isChildOfHtmlElement(node)) context.report({
@@ -26060,6 +26205,56 @@ const noBarrelImport = defineRule({
26060
26205
  }
26061
26206
  });
26062
26207
  //#endregion
26208
+ //#region src/plugin/utils/function-returns-matching-expression.ts
26209
+ const collectReturnedExpressions = (functionNode) => {
26210
+ if (!isFunctionLike$1(functionNode)) return [];
26211
+ const body = functionNode.body;
26212
+ if (!body) return [];
26213
+ if (!isNodeOfType(body, "BlockStatement")) return [body];
26214
+ const returnedExpressions = [];
26215
+ walkAst(body, (node) => {
26216
+ if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
26217
+ if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
26218
+ });
26219
+ return returnedExpressions;
26220
+ };
26221
+ const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
26222
+ const visitedExpressions = /* @__PURE__ */ new Set();
26223
+ const visitedFunctions = /* @__PURE__ */ new Set();
26224
+ const functionMatches = (candidateFunction) => {
26225
+ if (visitedFunctions.has(candidateFunction)) return false;
26226
+ visitedFunctions.add(candidateFunction);
26227
+ return collectReturnedExpressions(candidateFunction).some(expressionMatches);
26228
+ };
26229
+ const expressionMatches = (expression) => {
26230
+ const unwrappedExpression = stripParenExpression(expression);
26231
+ if (visitedExpressions.has(unwrappedExpression)) return false;
26232
+ visitedExpressions.add(unwrappedExpression);
26233
+ if (matchesExpression(unwrappedExpression)) return true;
26234
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
26235
+ const symbol = scopes.symbolFor(unwrappedExpression);
26236
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
26237
+ const initializer = stripParenExpression(symbol.initializer);
26238
+ if (isFunctionLike$1(initializer)) return false;
26239
+ return expressionMatches(initializer);
26240
+ }
26241
+ if (isNodeOfType(unwrappedExpression, "CallExpression")) {
26242
+ if (unwrappedExpression.arguments.length !== 0) return false;
26243
+ if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
26244
+ const symbol = scopes.symbolFor(unwrappedExpression.callee);
26245
+ if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
26246
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
26247
+ const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
26248
+ if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
26249
+ return functionMatches(candidateFunction);
26250
+ }
26251
+ if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
26252
+ if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
26253
+ return false;
26254
+ };
26255
+ return functionMatches(functionNode);
26256
+ };
26257
+ //#endregion
26063
26258
  //#region src/plugin/utils/function-contains-react-render-output.ts
26064
26259
  const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
26065
26260
  "FunctionDeclaration",
@@ -26079,12 +26274,13 @@ const isCallArgumentFunctionExpression = (node) => {
26079
26274
  return parent.arguments.some((argumentNode) => argumentNode === node);
26080
26275
  };
26081
26276
  const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
26277
+ const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
26082
26278
  const containsRenderOutput$1 = (rootNode, scopes) => {
26083
26279
  let hasRenderOutput = false;
26084
26280
  walkAst(rootNode, (node) => {
26085
26281
  if (hasRenderOutput) return false;
26086
26282
  if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
26087
- if (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
26283
+ if (isRenderOutputExpression(node, scopes)) {
26088
26284
  hasRenderOutput = true;
26089
26285
  return false;
26090
26286
  }
@@ -26095,7 +26291,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
26095
26291
  const functionContainsReactRenderOutput = (functionNode, scopes) => {
26096
26292
  const cachedEntry = renderOutputCache.get(functionNode);
26097
26293
  if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
26098
- const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
26294
+ const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
26099
26295
  renderOutputCache.set(functionNode, {
26100
26296
  scopes,
26101
26297
  hasRenderOutput
@@ -40123,20 +40319,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
40123
40319
  });
40124
40320
  return found;
40125
40321
  };
40322
+ const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
40126
40323
  const isReactClassComponent = (classNode) => {
40127
40324
  if (isEs6Component(classNode)) return true;
40128
40325
  return expressionContainsJsxOrCreateElement(classNode);
40129
40326
  };
40130
- const findEnclosingComponent = (node) => {
40327
+ const findEnclosingComponent = (node, scopes) => {
40131
40328
  let walker = node.parent;
40132
40329
  while (walker) {
40133
40330
  if (isFunctionLike$1(walker)) {
40134
40331
  const componentName = inferFunctionLikeName(walker);
40135
- if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
40332
+ if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
40136
40333
  component: walker,
40137
40334
  name: componentName
40138
40335
  };
40139
- if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
40336
+ if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
40140
40337
  component: walker,
40141
40338
  name: null
40142
40339
  };
@@ -40375,7 +40572,7 @@ const noUnstableNestedComponents = defineRule({
40375
40572
  if (renderPropRegex.test(propInfo.propName)) return;
40376
40573
  if (settings.allowAsProps) return;
40377
40574
  }
40378
- const enclosing = findEnclosingComponent(candidateNode);
40575
+ const enclosing = findEnclosingComponent(candidateNode, context.scopes);
40379
40576
  if (!enclosing) return;
40380
40577
  const gatedName = propInfo ? null : requiredInstantiationName;
40381
40578
  queuedReports.push({
@@ -40386,7 +40583,7 @@ const noUnstableNestedComponents = defineRule({
40386
40583
  });
40387
40584
  };
40388
40585
  const checkFunctionLike = (node) => {
40389
- if (!expressionContainsJsxOrCreateElement(node)) return;
40586
+ if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
40390
40587
  const inferredName = inferFunctionLikeName(node);
40391
40588
  const propInfo = isComponentDeclaredInProp(node);
40392
40589
  const isObjectCallback = isObjectCallbackCandidate(node);
@@ -45416,14 +45613,10 @@ const rerenderLazyStateInit = defineRule({
45416
45613
  //#endregion
45417
45614
  //#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
45418
45615
  const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
45419
- const callbackReturnsJsx = (callback) => {
45616
+ const callbackReturnsJsx = (callback, scopes) => {
45420
45617
  if (!callback) return false;
45421
45618
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
45422
- const body = callback.body;
45423
- if (isJsxExpression(body)) return true;
45424
- if (!isNodeOfType(body, "BlockStatement")) return false;
45425
- for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
45426
- return false;
45619
+ return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
45427
45620
  };
45428
45621
  const returnArgumentUsesAnyName = (returnStatement, names) => {
45429
45622
  if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
@@ -45501,7 +45694,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
45501
45694
  if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
45502
45695
  for (const declarator of stmt.declarations ?? []) {
45503
45696
  const init = declarator.init;
45504
- if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
45697
+ if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes)) {
45505
45698
  memoNode = declarator;
45506
45699
  callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
45507
45700
  if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.6-dev.8c9a6c4",
3
+ "version": "0.7.6-dev.a8cd24e",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",