oxlint-plugin-react-doctor 0.7.6-dev.e026a23 → 0.7.6-dev.e141d90

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 +432 -144
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1518,6 +1518,37 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
1518
1518
  }
1519
1519
  };
1520
1520
  //#endregion
1521
+ //#region src/plugin/utils/is-uppercase-name.ts
1522
+ const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
1523
+ //#endregion
1524
+ //#region src/plugin/utils/resolve-jsx-element-type.ts
1525
+ const resolveConstantStringBinding = (name) => {
1526
+ const binding = findVariableInitializer(name, name.name);
1527
+ if (!binding?.initializer) return null;
1528
+ const declarator = binding.bindingIdentifier.parent;
1529
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
1530
+ if (declarator.id !== binding.bindingIdentifier) return null;
1531
+ const declaration = declarator.parent;
1532
+ if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
1533
+ if (declaration.kind !== "const") return null;
1534
+ const initializer = stripParenExpression(binding.initializer);
1535
+ return isNodeOfType(initializer, "Literal") && typeof initializer.value === "string" ? initializer.value : null;
1536
+ };
1537
+ const flattenJsxName$2 = (name) => {
1538
+ if (isNodeOfType(name, "JSXIdentifier")) return name.name;
1539
+ if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
1540
+ if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
1541
+ return "";
1542
+ };
1543
+ const resolveJsxElementType = (openingElement) => {
1544
+ const name = openingElement.name;
1545
+ if (isNodeOfType(name, "JSXIdentifier")) {
1546
+ if (!isUppercaseName(name.name)) return name.name;
1547
+ return resolveConstantStringBinding(name) ?? name.name;
1548
+ }
1549
+ return flattenJsxName$2(name);
1550
+ };
1551
+ //#endregion
1521
1552
  //#region src/plugin/utils/get-element-type.ts
1522
1553
  const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
1523
1554
  const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
@@ -1530,14 +1561,8 @@ const readJsxA11ySettings = (settings) => {
1530
1561
  jsxA11ySettingsCache.set(settings, a11ySettings);
1531
1562
  return a11ySettings;
1532
1563
  };
1533
- const flattenJsxName$2 = (name) => {
1534
- if (isNodeOfType(name, "JSXIdentifier")) return name.name;
1535
- if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
1536
- if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
1537
- return "";
1538
- };
1539
1564
  const computeElementType = (openingElement, a11ySettings) => {
1540
- const baseName = flattenJsxName$2(openingElement.name);
1565
+ const baseName = resolveJsxElementType(openingElement);
1541
1566
  if (a11ySettings.polymorphicPropName) {
1542
1567
  const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
1543
1568
  if (polymorphicAttribute) {
@@ -2372,8 +2397,9 @@ const anchorIsValid = defineRule({
2372
2397
  const isTestlikeFile = isTestlikeFilename(context.filename);
2373
2398
  return { JSXOpeningElement(node) {
2374
2399
  if (isTestlikeFile) return;
2375
- if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
2376
- if (getElementType(node, context.settings) !== "a") return;
2400
+ const tag = getElementType(node, context.settings);
2401
+ if (!fileHasJsxA11ySettings && tag !== "a") return;
2402
+ if (tag !== "a") return;
2377
2403
  let hrefAttribute;
2378
2404
  for (const attributeName of settings.hrefAttributeNames) {
2379
2405
  hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
@@ -5728,7 +5754,7 @@ const buttonHasType = defineRule({
5728
5754
  return {
5729
5755
  JSXOpeningElement(node) {
5730
5756
  if (isTestlikeFile) return;
5731
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "button") return;
5757
+ if (resolveJsxElementType(node) !== "button") return;
5732
5758
  const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
5733
5759
  if (!typeAttr) {
5734
5760
  if (hasJsxSpreadAttribute$1(node.attributes)) {
@@ -5918,7 +5944,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
5918
5944
  };
5919
5945
  return {
5920
5946
  JSXOpeningElement(node) {
5921
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
5947
+ if (resolveJsxElementType(node) !== "input") return;
5922
5948
  reportFromPresence(collectFromJsxAttributes(node.attributes));
5923
5949
  },
5924
5950
  CallExpression(node) {
@@ -5995,7 +6021,7 @@ const isPureEventBlockerHandler = (attribute) => {
5995
6021
  //#endregion
5996
6022
  //#region src/plugin/utils/resolve-const-identifier-alias.ts
5997
6023
  const resolveConstIdentifierAlias = (identifier, scopes) => {
5998
- if (!isNodeOfType(identifier, "Identifier")) return null;
6024
+ if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
5999
6025
  const visitedSymbolIds = /* @__PURE__ */ new Set();
6000
6026
  let symbol = scopes.symbolFor(identifier);
6001
6027
  while (symbol?.kind === "const") {
@@ -6270,6 +6296,131 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
6270
6296
  return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
6271
6297
  };
6272
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
6273
6424
  //#region src/plugin/rules/client/client-localstorage-no-version.ts
6274
6425
  const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
6275
6426
  const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
@@ -6291,26 +6442,39 @@ const clientLocalstorageNoVersion = defineRule({
6291
6442
  severity: "warn",
6292
6443
  category: "Correctness",
6293
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.",
6294
- create: (context) => ({ CallExpression(node) {
6295
- if (!isNodeOfType(node.callee, "MemberExpression")) return;
6296
- const receiver = stripParenExpression(node.callee.object);
6297
- if (!isNodeOfType(receiver, "Identifier")) return;
6298
- if (receiver.name !== "localStorage") return;
6299
- if (!isNodeOfType(node.callee.property, "Identifier")) return;
6300
- if (node.callee.property.name !== "setItem") return;
6301
- const keyArg = node.arguments?.[0];
6302
- if (!keyArg) return;
6303
- const keyValue = resolveStringKey(keyArg, context);
6304
- if (keyValue === null) return;
6305
- if (isVersionedKey(keyValue)) return;
6306
- const valueArg = node.arguments?.[1];
6307
- if (!valueArg) return;
6308
- if (!isJsonStringifyCall(valueArg)) return;
6309
- context.report({
6310
- node: keyArg,
6311
- 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").`
6312
- });
6313
- } })
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
+ }
6314
6478
  });
6315
6479
  //#endregion
6316
6480
  //#region src/plugin/rules/client/client-passive-event-listeners.ts
@@ -7433,7 +7597,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
7433
7597
  const getOpeningElementTagName = (openingElement) => {
7434
7598
  if (!openingElement) return null;
7435
7599
  if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
7436
- if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement.name.name;
7600
+ if (isNodeOfType(openingElement.name, "JSXIdentifier")) return resolveJsxElementType(openingElement);
7437
7601
  if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
7438
7602
  let cursor = openingElement.name;
7439
7603
  while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
@@ -7685,7 +7849,7 @@ const dialogHasAccessibleName = defineRule({
7685
7849
  if (isTestlikeFilename(context.filename)) return {};
7686
7850
  return { JSXOpeningElement(node) {
7687
7851
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
7688
- const tagName = node.name.name;
7852
+ const tagName = resolveJsxElementType(node);
7689
7853
  if (tagName[0] !== tagName[0]?.toLowerCase()) return;
7690
7854
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
7691
7855
  const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
@@ -11917,7 +12081,7 @@ const forbidDomProps = defineRule({
11917
12081
  return { JSXOpeningElement(node) {
11918
12082
  if (forbidMap.size === 0) return;
11919
12083
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
11920
- const elementName = node.name.name;
12084
+ const elementName = resolveJsxElementType(node);
11921
12085
  if (isReactComponentName(elementName)) return;
11922
12086
  for (const attribute of node.attributes) {
11923
12087
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -11995,7 +12159,7 @@ const forbidElements = defineRule({
11995
12159
  return {
11996
12160
  JSXOpeningElement(node) {
11997
12161
  if (forbidMap.size === 0) return;
11998
- const fullName = flattenJsxName$1(node.name);
12162
+ const fullName = resolveJsxElementType(node);
11999
12163
  if (!fullName || !forbidMap.has(fullName)) return;
12000
12164
  context.report({
12001
12165
  node: node.name,
@@ -12357,8 +12521,7 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
12357
12521
  const isParagraphElement = (candidate) => {
12358
12522
  if (!isNodeOfType(candidate, "JSXElement")) return false;
12359
12523
  const opening = candidate.openingElement;
12360
- if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
12361
- return opening.name.name === "p";
12524
+ return resolveJsxElementType(opening) === "p";
12362
12525
  };
12363
12526
  const findEnclosingParagraph = (openingElement) => {
12364
12527
  const owningElement = openingElement.parent;
@@ -12379,8 +12542,7 @@ const htmlNoInvalidParagraphChild = defineRule({
12379
12542
  severity: "warn",
12380
12543
  recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
12381
12544
  create: (context) => ({ JSXOpeningElement(node) {
12382
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
12383
- const childTagName = node.name.name;
12545
+ const childTagName = resolveJsxElementType(node);
12384
12546
  if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
12385
12547
  if (!findEnclosingParagraph(node)) return;
12386
12548
  context.report({
@@ -12411,7 +12573,7 @@ const getHostTagName = (jsxElement) => {
12411
12573
  if (!isNodeOfType(jsxElement, "JSXElement")) return null;
12412
12574
  const opening = jsxElement.openingElement;
12413
12575
  if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
12414
- const tagName = opening.name.name;
12576
+ const tagName = resolveJsxElementType(opening);
12415
12577
  if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
12416
12578
  return tagName;
12417
12579
  };
@@ -12441,7 +12603,7 @@ const findClosestHostAncestor = (jsxElement) => {
12441
12603
  if (isNodeOfType(ancestor, "JSXElement")) {
12442
12604
  const opening = ancestor.openingElement;
12443
12605
  if (isNodeOfType(opening.name, "JSXIdentifier")) {
12444
- const ancestorTag = opening.name.name;
12606
+ const ancestorTag = resolveJsxElementType(opening);
12445
12607
  if (ancestorTag.length === 0) {
12446
12608
  previous = ancestor;
12447
12609
  ancestor = ancestor.parent ?? null;
@@ -12523,8 +12685,7 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
12523
12685
  const isJsxElementWithTagName = (candidate, tagName) => {
12524
12686
  if (!isNodeOfType(candidate, "JSXElement")) return false;
12525
12687
  const opening = candidate.openingElement;
12526
- if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
12527
- return opening.name.name === tagName;
12688
+ return resolveJsxElementType(opening) === tagName;
12528
12689
  };
12529
12690
  const findEnclosingSameTag = (openingElement, tagName) => {
12530
12691
  const owningElement = openingElement.parent;
@@ -12545,8 +12706,7 @@ const htmlNoNestedInteractive = defineRule({
12545
12706
  severity: "warn",
12546
12707
  recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
12547
12708
  create: (context) => ({ JSXOpeningElement(node) {
12548
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
12549
- const tagName = node.name.name;
12709
+ const tagName = resolveJsxElementType(node);
12550
12710
  if (tagName !== "a" && tagName !== "button") return;
12551
12711
  if (!findEnclosingSameTag(node, tagName)) return;
12552
12712
  context.report({
@@ -12661,7 +12821,7 @@ const iframeMissingSandbox = defineRule({
12661
12821
  matchByOccurrence: true,
12662
12822
  create: skipNonProductionFiles((context) => ({
12663
12823
  JSXOpeningElement(node) {
12664
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "iframe") return;
12824
+ if (resolveJsxElementType(node) !== "iframe") return;
12665
12825
  const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
12666
12826
  if (!sandboxAttr) {
12667
12827
  const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
@@ -14589,6 +14749,18 @@ const jsHoistRegexp = defineRule({
14589
14749
  }, { treatIteratorCallbacksAsLoops: true })
14590
14750
  });
14591
14751
  //#endregion
14752
+ //#region src/plugin/utils/resolve-first-argument-binding.ts
14753
+ const resolveFirstArgumentBinding = (firstParameter) => {
14754
+ if (!firstParameter) return null;
14755
+ if (!isNodeOfType(firstParameter, "RestElement")) return firstParameter;
14756
+ if (!isNodeOfType(firstParameter.argument, "ArrayPattern")) return null;
14757
+ const elements = firstParameter.argument.elements ?? [];
14758
+ if (elements.length !== 1) return null;
14759
+ const firstBinding = elements[0];
14760
+ if (!firstBinding || isNodeOfType(firstBinding, "RestElement")) return null;
14761
+ return firstBinding;
14762
+ };
14763
+ //#endregion
14592
14764
  //#region src/plugin/rules/js-performance/js-index-maps.ts
14593
14765
  const referencesParameter = (expression, parameterName) => {
14594
14766
  if (!expression) return false;
@@ -14599,7 +14771,7 @@ const referencesParameter = (expression, parameterName) => {
14599
14771
  const isSingleFieldEqualityPredicate = (node) => {
14600
14772
  const callback = node.arguments?.[0];
14601
14773
  if (!isInlineFunctionExpression(callback)) return false;
14602
- const firstParameter = callback.params?.[0];
14774
+ const firstParameter = resolveFirstArgumentBinding(callback.params?.[0]);
14603
14775
  if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
14604
14776
  let predicate = null;
14605
14777
  const body = callback.body;
@@ -15938,14 +16110,21 @@ const jsxFilenameExtension = defineRule({
15938
16110
  });
15939
16111
  //#endregion
15940
16112
  //#region src/plugin/utils/is-jsx-fragment-element.ts
15941
- const isJsxFragmentElement = (node) => {
16113
+ const isJsxFragmentElement = (node, scopes) => {
15942
16114
  if (!isNodeOfType(node, "JSXOpeningElement")) return false;
15943
16115
  const elementName = node.name;
15944
- 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
+ }
15945
16122
  if (isNodeOfType(elementName, "JSXMemberExpression")) {
15946
16123
  if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
15947
- if (elementName.object.name !== "React") return false;
15948
- 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);
15949
16128
  }
15950
16129
  return false;
15951
16130
  };
@@ -15971,7 +16150,7 @@ const jsxFragments = defineRule({
15971
16150
  if (mode !== "syntax") return;
15972
16151
  if (!node.closingElement) return;
15973
16152
  const openingElement = node.openingElement;
15974
- if (!isJsxFragmentElement(openingElement)) return;
16153
+ if (!isJsxFragmentElement(openingElement, context.scopes)) return;
15975
16154
  if (openingElement.attributes.length > 0) return;
15976
16155
  context.report({
15977
16156
  node: openingElement,
@@ -18652,10 +18831,6 @@ const resolveSettings$29 = (settings) => {
18652
18831
  if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
18653
18832
  return reactDoctor.jsxNoScriptUrl ?? {};
18654
18833
  };
18655
- const getElementName = (node) => {
18656
- if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
18657
- return null;
18658
- };
18659
18834
  const isLinkPropForElement = (elementName, attributeName, options) => {
18660
18835
  if (elementName === "a" && attributeName === "href") return true;
18661
18836
  const explicit = options.components?.[elementName];
@@ -18675,7 +18850,8 @@ const jsxNoScriptUrl = defineRule({
18675
18850
  create: (context) => {
18676
18851
  const options = resolveSettings$29(context.settings);
18677
18852
  return { JSXOpeningElement(node) {
18678
- const elementName = getElementName(node);
18853
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
18854
+ const elementName = resolveJsxElementType(node);
18679
18855
  if (!elementName) return;
18680
18856
  for (const attribute of node.attributes) {
18681
18857
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -18862,11 +19038,6 @@ const checkTarget = (attributeValue) => {
18862
19038
  alternate: false
18863
19039
  };
18864
19040
  };
18865
- const getOpeningElementName = (node) => {
18866
- const name = node.name;
18867
- if (isNodeOfType(name, "JSXIdentifier")) return name.name;
18868
- return null;
18869
- };
18870
19041
  const jsxNoTargetBlank = defineRule({
18871
19042
  id: "jsx-no-target-blank",
18872
19043
  title: "Unsafe target=_blank link",
@@ -18886,7 +19057,8 @@ const jsxNoTargetBlank = defineRule({
18886
19057
  return settings.formComponents.has(tagName);
18887
19058
  };
18888
19059
  return { JSXOpeningElement(node) {
18889
- const tagName = getOpeningElementName(node);
19060
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
19061
+ const tagName = resolveJsxElementType(node);
18890
19062
  if (!tagName) return;
18891
19063
  if (!isLink(tagName) && !isForm(tagName)) return;
18892
19064
  const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
@@ -19119,7 +19291,7 @@ const jsxNoUselessFragment = defineRule({
19119
19291
  return {
19120
19292
  JSXElement(node) {
19121
19293
  const openingElement = node.openingElement;
19122
- if (!isJsxFragmentElement(openingElement)) return;
19294
+ if (!isJsxFragmentElement(openingElement, context.scopes)) return;
19123
19295
  if (hasJsxKeyAttribute(openingElement)) return;
19124
19296
  if (checkChildren(node, openingElement, node.children)) return;
19125
19297
  if (isChildOfHtmlElement(node)) context.report({
@@ -20361,11 +20533,24 @@ const hasDirective = (programNode, directive) => {
20361
20533
  return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
20362
20534
  };
20363
20535
  //#endregion
20364
- //#region src/plugin/utils/is-uppercase-name.ts
20365
- const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
20366
- //#endregion
20367
- //#region src/plugin/utils/is-component-assignment.ts
20368
- 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
+ };
20369
20554
  //#endregion
20370
20555
  //#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
20371
20556
  const nextjsAsyncClientComponent = defineRule({
@@ -20391,10 +20576,10 @@ const nextjsAsyncClientComponent = defineRule({
20391
20576
  },
20392
20577
  VariableDeclarator(node) {
20393
20578
  if (!fileHasUseClient) return;
20394
- if (!isComponentAssignment(node)) return;
20395
- if (!isInlineFunctionExpression(node.init)) return;
20396
- if (!node.init.async) return;
20397
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;
20398
20583
  context.report({
20399
20584
  node,
20400
20585
  message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
@@ -20696,7 +20881,7 @@ const nextjsNoAElement = defineRule({
20696
20881
  severity: "warn",
20697
20882
  recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
20698
20883
  create: (context) => ({ JSXOpeningElement(node) {
20699
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
20884
+ if (resolveJsxElementType(node) !== "a") return;
20700
20885
  const attributes = node.attributes ?? [];
20701
20886
  const downloadAttribute = findJsxAttribute(attributes, "download");
20702
20887
  if (downloadAttribute) {
@@ -20892,7 +21077,7 @@ const nextjsNoCssLink = defineRule({
20892
21077
  severity: "warn",
20893
21078
  recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
20894
21079
  create: (context) => ({ JSXOpeningElement(node) {
20895
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
21080
+ if (resolveJsxElementType(node) !== "link") return;
20896
21081
  const attributes = node.attributes ?? [];
20897
21082
  const relAttribute = findJsxAttribute(attributes, "rel");
20898
21083
  if (!relAttribute?.value) return;
@@ -21000,7 +21185,7 @@ const nextjsNoFontLink = defineRule({
21000
21185
  severity: "warn",
21001
21186
  recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
21002
21187
  create: (context) => ({ JSXOpeningElement(node) {
21003
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
21188
+ if (resolveJsxElementType(node) !== "link") return;
21004
21189
  const attributes = node.attributes ?? [];
21005
21190
  const hrefAttribute = findJsxAttribute(attributes, "href");
21006
21191
  if (!hrefAttribute?.value) return;
@@ -21175,7 +21360,7 @@ const nextjsNoImgElement = defineRule({
21175
21360
  create: (context) => {
21176
21361
  if (isGeneratedImageRenderContext(context)) return {};
21177
21362
  return { JSXOpeningElement(node) {
21178
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
21363
+ if (resolveJsxElementType(node) !== "img") return;
21179
21364
  if (isGeneratedImageRenderContext(context, node)) return;
21180
21365
  const programRoot = findProgramRoot(node);
21181
21366
  if (programRoot && hasEmailTemplateImport(programRoot)) return;
@@ -21222,8 +21407,8 @@ const nextjsNoPolyfillScript = defineRule({
21222
21407
  severity: "warn",
21223
21408
  recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
21224
21409
  create: (context) => ({ JSXOpeningElement(node) {
21225
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
21226
- if (node.name.name !== "script" && node.name.name !== "Script") return;
21410
+ const elementName = resolveJsxElementType(node);
21411
+ if (elementName !== "script" && elementName !== "Script") return;
21227
21412
  const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
21228
21413
  if (!srcAttribute?.value) return;
21229
21414
  const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
@@ -23031,10 +23216,15 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
23031
23216
  if (!declaringNode) return false;
23032
23217
  return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
23033
23218
  }));
23219
+ const isWholePropsParameterBinding = (bindingNode) => {
23220
+ if (isFunctionLike$1(bindingNode.parent)) return true;
23221
+ let declaringFunction = bindingNode.parent;
23222
+ while (declaringFunction && !isFunctionLike$1(declaringFunction)) declaringFunction = declaringFunction.parent;
23223
+ return Boolean(declaringFunction && resolveFirstArgumentBinding(declaringFunction.params?.[0]) === bindingNode);
23224
+ };
23034
23225
  const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
23035
23226
  if (def.type !== "Parameter") return false;
23036
- const bindingParent = def.name.parent;
23037
- return isFunctionLike$1(bindingParent);
23227
+ return isWholePropsParameterBinding(def.name);
23038
23228
  }));
23039
23229
  const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
23040
23230
  const isPropAlias = (analysis, ref) => {
@@ -26031,6 +26221,56 @@ const noBarrelImport = defineRule({
26031
26221
  }
26032
26222
  });
26033
26223
  //#endregion
26224
+ //#region src/plugin/utils/function-returns-matching-expression.ts
26225
+ const collectReturnedExpressions = (functionNode) => {
26226
+ if (!isFunctionLike$1(functionNode)) return [];
26227
+ const body = functionNode.body;
26228
+ if (!body) return [];
26229
+ if (!isNodeOfType(body, "BlockStatement")) return [body];
26230
+ const returnedExpressions = [];
26231
+ walkAst(body, (node) => {
26232
+ if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
26233
+ if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
26234
+ });
26235
+ return returnedExpressions;
26236
+ };
26237
+ const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
26238
+ const visitedExpressions = /* @__PURE__ */ new Set();
26239
+ const visitedFunctions = /* @__PURE__ */ new Set();
26240
+ const functionMatches = (candidateFunction) => {
26241
+ if (visitedFunctions.has(candidateFunction)) return false;
26242
+ visitedFunctions.add(candidateFunction);
26243
+ return collectReturnedExpressions(candidateFunction).some(expressionMatches);
26244
+ };
26245
+ const expressionMatches = (expression) => {
26246
+ const unwrappedExpression = stripParenExpression(expression);
26247
+ if (visitedExpressions.has(unwrappedExpression)) return false;
26248
+ visitedExpressions.add(unwrappedExpression);
26249
+ if (matchesExpression(unwrappedExpression)) return true;
26250
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
26251
+ const symbol = scopes.symbolFor(unwrappedExpression);
26252
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
26253
+ const initializer = stripParenExpression(symbol.initializer);
26254
+ if (isFunctionLike$1(initializer)) return false;
26255
+ return expressionMatches(initializer);
26256
+ }
26257
+ if (isNodeOfType(unwrappedExpression, "CallExpression")) {
26258
+ if (unwrappedExpression.arguments.length !== 0) return false;
26259
+ if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
26260
+ const symbol = scopes.symbolFor(unwrappedExpression.callee);
26261
+ if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
26262
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
26263
+ const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
26264
+ if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
26265
+ return functionMatches(candidateFunction);
26266
+ }
26267
+ if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
26268
+ if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
26269
+ return false;
26270
+ };
26271
+ return functionMatches(functionNode);
26272
+ };
26273
+ //#endregion
26034
26274
  //#region src/plugin/utils/function-contains-react-render-output.ts
26035
26275
  const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
26036
26276
  "FunctionDeclaration",
@@ -26050,12 +26290,13 @@ const isCallArgumentFunctionExpression = (node) => {
26050
26290
  return parent.arguments.some((argumentNode) => argumentNode === node);
26051
26291
  };
26052
26292
  const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
26293
+ const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
26053
26294
  const containsRenderOutput$1 = (rootNode, scopes) => {
26054
26295
  let hasRenderOutput = false;
26055
26296
  walkAst(rootNode, (node) => {
26056
26297
  if (hasRenderOutput) return false;
26057
26298
  if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
26058
- if (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
26299
+ if (isRenderOutputExpression(node, scopes)) {
26059
26300
  hasRenderOutput = true;
26060
26301
  return false;
26061
26302
  }
@@ -26066,7 +26307,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
26066
26307
  const functionContainsReactRenderOutput = (functionNode, scopes) => {
26067
26308
  const cachedEntry = renderOutputCache.get(functionNode);
26068
26309
  if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
26069
- const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
26310
+ const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
26070
26311
  renderOutputCache.set(functionNode, {
26071
26312
  scopes,
26072
26313
  hasRenderOutput
@@ -26074,6 +26315,9 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
26074
26315
  return hasRenderOutput;
26075
26316
  };
26076
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
26077
26321
  //#region src/plugin/utils/is-component-declaration.ts
26078
26322
  const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
26079
26323
  //#endregion
@@ -28531,7 +28775,7 @@ const noDisabledZoom = defineRule({
28531
28775
  category: "Accessibility",
28532
28776
  recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
28533
28777
  create: (context) => ({ JSXOpeningElement(node) {
28534
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
28778
+ if (resolveJsxElementType(node) !== "meta") return;
28535
28779
  const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
28536
28780
  if (!nameAttr?.value) return;
28537
28781
  if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
@@ -31703,7 +31947,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
31703
31947
  severity: "warn",
31704
31948
  recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
31705
31949
  create: (context) => ({ JSXOpeningElement(node) {
31706
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
31950
+ if (resolveJsxElementType(node) !== "img") return;
31707
31951
  const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
31708
31952
  if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
31709
31953
  const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
@@ -31944,7 +32188,7 @@ const noIndeterminateAttribute = defineRule({
31944
32188
  if (classifyReactNativeFileTarget(context) === "react-native") return {};
31945
32189
  return {
31946
32190
  JSXOpeningElement(node) {
31947
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
32191
+ if (resolveJsxElementType(node) !== "input") return;
31948
32192
  let typeAttribute = null;
31949
32193
  let typeAttributeIndex = null;
31950
32194
  let indeterminateAttribute = null;
@@ -32123,11 +32367,12 @@ const isMemoCall = (node) => {
32123
32367
  };
32124
32368
  const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
32125
32369
  const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
32126
- const isInlineReference = (node) => {
32127
- 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";
32128
- if (isNodeOfType(node, "ObjectExpression")) return "objects";
32129
- if (isNodeOfType(node, "ArrayExpression")) return "Arrays";
32130
- 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";
32131
32376
  return null;
32132
32377
  };
32133
32378
  const noInlinePropOnMemoComponent = defineRule({
@@ -32157,7 +32402,7 @@ const noInlinePropOnMemoComponent = defineRule({
32157
32402
  let elementName = null;
32158
32403
  if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
32159
32404
  if (!elementName || !memoizedComponentNames.has(elementName)) return;
32160
- const propType = isInlineReference(node.value.expression);
32405
+ const propType = isInlineReference(node.value.expression, context.scopes);
32161
32406
  if (propType) context.report({
32162
32407
  node: node.value.expression,
32163
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`
@@ -33016,11 +33261,13 @@ const noManyBooleanProps = defineRule({
33016
33261
  };
33017
33262
  const checkComponent = (functionNode, param, body, componentName, reportNode) => {
33018
33263
  if (!param) return;
33264
+ const propsBinding = resolveFirstArgumentBinding(param);
33265
+ if (!propsBinding) return;
33019
33266
  if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
33020
- if (isNodeOfType(param, "ObjectPattern")) {
33267
+ if (isNodeOfType(propsBinding, "ObjectPattern")) {
33021
33268
  const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
33022
33269
  const booleanLikePropNames = [];
33023
- for (const property of param.properties ?? []) {
33270
+ for (const property of propsBinding.properties ?? []) {
33024
33271
  if (!isNodeOfType(property, "Property")) continue;
33025
33272
  const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
33026
33273
  if (!keyName) continue;
@@ -33031,7 +33278,7 @@ const noManyBooleanProps = defineRule({
33031
33278
  reportIfMany(booleanLikePropNames, componentName, reportNode);
33032
33279
  return;
33033
33280
  }
33034
- if (isNodeOfType(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
33281
+ if (isNodeOfType(propsBinding, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, propsBinding.name)], componentName, reportNode);
33035
33282
  };
33036
33283
  return {
33037
33284
  FunctionDeclaration(node) {
@@ -35783,7 +36030,7 @@ const noPreventDefault = defineRule({
35783
36030
  const isServerActionsFramework = hasCapability(context.settings, "server-actions");
35784
36031
  const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
35785
36032
  return { JSXOpeningElement(node) {
35786
- const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
36033
+ const elementName = resolveJsxElementType(node);
35787
36034
  if (!elementName) return;
35788
36035
  const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
35789
36036
  if (!targetEventProps) return;
@@ -38247,9 +38494,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
38247
38494
  recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
38248
38495
  create: (context) => ({ JSXOpeningElement(node) {
38249
38496
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
38250
- const firstCharacter = node.name.name.charCodeAt(0);
38497
+ const elementName = resolveJsxElementType(node);
38498
+ const firstCharacter = elementName.charCodeAt(0);
38251
38499
  if (firstCharacter < 97 || firstCharacter > 122) return;
38252
- if (node.name.name.includes("-")) return;
38500
+ if (elementName.includes("-")) return;
38253
38501
  for (const attribute of node.attributes) {
38254
38502
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
38255
38503
  if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
@@ -38637,8 +38885,7 @@ const noUncontrolledInput = defineRule({
38637
38885
  const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
38638
38886
  walkAst(componentBody, (child) => {
38639
38887
  if (!isNodeOfType(child, "JSXOpeningElement")) return;
38640
- if (!isNodeOfType(child.name, "JSXIdentifier")) return;
38641
- const tagName = child.name.name;
38888
+ const tagName = resolveJsxElementType(child);
38642
38889
  if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
38643
38890
  const attributes = child.attributes ?? [];
38644
38891
  if (hasJsxSpreadAttribute(attributes)) return;
@@ -38699,7 +38946,7 @@ const noUndeferredThirdParty = defineRule({
38699
38946
  severity: "warn",
38700
38947
  recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
38701
38948
  create: (context) => ({ JSXOpeningElement(node) {
38702
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
38949
+ if (resolveJsxElementType(node) !== "script") return;
38703
38950
  const attributes = node.attributes ?? [];
38704
38951
  const srcAttribute = findJsxAttribute(attributes, "src");
38705
38952
  if (!srcAttribute) return;
@@ -39914,7 +40161,7 @@ const noUnknownProperty = defineRule({
39914
40161
  }
39915
40162
  if (fileIsNonReactJsx) return;
39916
40163
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
39917
- const elementType = node.name.name;
40164
+ const elementType = resolveJsxElementType(node);
39918
40165
  const firstCharacter = elementType.charCodeAt(0);
39919
40166
  if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
39920
40167
  let isValidHtmlTag = isKnownDomTag(elementType);
@@ -40092,20 +40339,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
40092
40339
  });
40093
40340
  return found;
40094
40341
  };
40342
+ const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
40095
40343
  const isReactClassComponent = (classNode) => {
40096
40344
  if (isEs6Component(classNode)) return true;
40097
40345
  return expressionContainsJsxOrCreateElement(classNode);
40098
40346
  };
40099
- const findEnclosingComponent = (node) => {
40347
+ const findEnclosingComponent = (node, scopes) => {
40100
40348
  let walker = node.parent;
40101
40349
  while (walker) {
40102
40350
  if (isFunctionLike$1(walker)) {
40103
40351
  const componentName = inferFunctionLikeName(walker);
40104
- if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
40352
+ if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
40105
40353
  component: walker,
40106
40354
  name: componentName
40107
40355
  };
40108
- if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
40356
+ if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
40109
40357
  component: walker,
40110
40358
  name: null
40111
40359
  };
@@ -40344,7 +40592,7 @@ const noUnstableNestedComponents = defineRule({
40344
40592
  if (renderPropRegex.test(propInfo.propName)) return;
40345
40593
  if (settings.allowAsProps) return;
40346
40594
  }
40347
- const enclosing = findEnclosingComponent(candidateNode);
40595
+ const enclosing = findEnclosingComponent(candidateNode, context.scopes);
40348
40596
  if (!enclosing) return;
40349
40597
  const gatedName = propInfo ? null : requiredInstantiationName;
40350
40598
  queuedReports.push({
@@ -40355,7 +40603,7 @@ const noUnstableNestedComponents = defineRule({
40355
40603
  });
40356
40604
  };
40357
40605
  const checkFunctionLike = (node) => {
40358
- if (!expressionContainsJsxOrCreateElement(node)) return;
40606
+ if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
40359
40607
  const inferredName = inferFunctionLikeName(node);
40360
40608
  const propInfo = isComponentDeclaredInProp(node);
40361
40609
  const isObjectCallback = isObjectCallbackCandidate(node);
@@ -41615,7 +41863,7 @@ const preactPreferOndblclick = defineRule({
41615
41863
  recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
41616
41864
  create: (context) => ({ JSXOpeningElement(node) {
41617
41865
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
41618
- const tagName = node.name.name;
41866
+ const tagName = resolveJsxElementType(node);
41619
41867
  if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
41620
41868
  const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
41621
41869
  if (!onDoubleClickAttribute) return;
@@ -41635,7 +41883,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
41635
41883
  ]);
41636
41884
  const isTextLikeInput = (openingElement) => {
41637
41885
  if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
41638
- const tagName = openingElement.name.name;
41886
+ const tagName = resolveJsxElementType(openingElement);
41639
41887
  if (tagName === "textarea") return true;
41640
41888
  if (tagName !== "input") return false;
41641
41889
  const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
@@ -41792,8 +42040,9 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
41792
42040
  ]);
41793
42041
  const collectBooleanPropBindings = (param) => {
41794
42042
  const bindings = /* @__PURE__ */ new Set();
41795
- if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
41796
- for (const property of param.properties ?? []) {
42043
+ const propsBinding = resolveFirstArgumentBinding(param);
42044
+ if (!isNodeOfType(propsBinding, "ObjectPattern")) return bindings;
42045
+ for (const property of propsBinding.properties ?? []) {
41797
42046
  if (!isNodeOfType(property, "Property")) continue;
41798
42047
  if (property.computed) continue;
41799
42048
  if (!isNodeOfType(property.key, "Identifier")) continue;
@@ -42054,7 +42303,7 @@ const preferHtmlDialog = defineRule({
42054
42303
  },
42055
42304
  JSXOpeningElement(node) {
42056
42305
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
42057
- const tagName = node.name.name;
42306
+ const tagName = resolveJsxElementType(node);
42058
42307
  if (tagName === "dialog") return;
42059
42308
  if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
42060
42309
  if (!HTML_TAGS.has(tagName)) return;
@@ -43601,13 +43850,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
43601
43850
  }
43602
43851
  return null;
43603
43852
  };
43604
- const resolveHookNameFromInitializer = (initializer) => {
43853
+ const resolveHookNameFromInitializer = (initializer, scopes) => {
43605
43854
  if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
43606
43855
  if (!isNodeOfType(initializer, "Identifier")) return null;
43607
- const binding = findVariableInitializer(initializer, initializer.name);
43608
- if (!binding?.initializer || !isConstDeclaredBinding(binding)) return null;
43609
- if (!isNodeOfType(binding.initializer, "CallExpression")) return null;
43610
- 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);
43611
43860
  };
43612
43861
  const queryNoRestDestructuring = defineRule({
43613
43862
  id: "query-no-rest-destructuring",
@@ -43620,7 +43869,7 @@ const queryNoRestDestructuring = defineRule({
43620
43869
  if (!isNodeOfType(node.id, "ObjectPattern")) return;
43621
43870
  if (!node.init) return;
43622
43871
  if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
43623
- const hookName = resolveHookNameFromInitializer(node.init);
43872
+ const hookName = resolveHookNameFromInitializer(node.init, context.scopes);
43624
43873
  if (!hookName) return;
43625
43874
  context.report({
43626
43875
  node: node.id,
@@ -44091,7 +44340,7 @@ const renderingAnimateSvgWrapper = defineRule({
44091
44340
  severity: "warn",
44092
44341
  recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
44093
44342
  create: (context) => ({ JSXOpeningElement(node) {
44094
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
44343
+ if (resolveJsxElementType(node) !== "svg") return;
44095
44344
  if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
44096
44345
  node,
44097
44346
  message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
@@ -44975,7 +45224,7 @@ const DEFERRABLE_HOOK_NAMES = new Set([
44975
45224
  "useParams",
44976
45225
  "usePathname"
44977
45226
  ]);
44978
- const findHookCallBindings = (componentBody) => {
45227
+ const findHookCallBindings = (componentBody, scopes) => {
44979
45228
  const bindings = [];
44980
45229
  if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
44981
45230
  for (const statement of componentBody.body ?? []) {
@@ -44986,8 +45235,10 @@ const findHookCallBindings = (componentBody) => {
44986
45235
  const callee = declarator.init.callee;
44987
45236
  if (!isNodeOfType(callee, "Identifier")) continue;
44988
45237
  if (!DEFERRABLE_HOOK_NAMES.has(callee.name)) continue;
45238
+ const valueSymbol = scopes.symbolFor(declarator.id);
45239
+ if (!valueSymbol) continue;
44989
45240
  bindings.push({
44990
- valueName: declarator.id.name,
45241
+ valueSymbol,
44991
45242
  hookName: callee.name,
44992
45243
  declarator
44993
45244
  });
@@ -44995,6 +45246,49 @@ const findHookCallBindings = (componentBody) => {
44995
45246
  }
44996
45247
  return bindings;
44997
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
+ };
44998
45292
  const rerenderDeferReadsHook = defineRule({
44999
45293
  id: "rerender-defer-reads-hook",
45000
45294
  title: "URL hook value only read in handlers",
@@ -45005,15 +45299,13 @@ const rerenderDeferReadsHook = defineRule({
45005
45299
  create: (context) => {
45006
45300
  const checkComponent = (componentBody) => {
45007
45301
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
45008
- const bindings = findHookCallBindings(componentBody);
45302
+ const bindings = findHookCallBindings(componentBody, context.scopes);
45009
45303
  if (bindings.length === 0) return;
45010
45304
  const handlerBindingNames = collectHandlerBindingNames(componentBody);
45011
45305
  for (const binding of bindings) {
45306
+ const { symbols, aliasSourceIdentifiers } = collectExactAliasSymbols(componentBody, binding.valueSymbol, context.scopes);
45012
45307
  const referenceLocations = [];
45013
- walkAst(componentBody, (child) => {
45014
- if (isNodeOfType(binding.declarator, "VariableDeclarator") && child === binding.declarator.id) return;
45015
- if (isNodeOfType(child, "Identifier") && child.name === binding.valueName) referenceLocations.push(child);
45016
- });
45308
+ for (const symbol of symbols) for (const reference of symbol.references) if (!aliasSourceIdentifiers.has(reference.identifier)) referenceLocations.push(reference.identifier);
45017
45309
  if (referenceLocations.length === 0) continue;
45018
45310
  if (!referenceLocations.every((ref) => isInsideEventHandler(ref, handlerBindingNames))) continue;
45019
45311
  context.report({
@@ -45384,14 +45676,10 @@ const rerenderLazyStateInit = defineRule({
45384
45676
  //#endregion
45385
45677
  //#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
45386
45678
  const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
45387
- const callbackReturnsJsx = (callback) => {
45679
+ const callbackReturnsJsx = (callback, scopes) => {
45388
45680
  if (!callback) return false;
45389
45681
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
45390
- const body = callback.body;
45391
- if (isJsxExpression(body)) return true;
45392
- if (!isNodeOfType(body, "BlockStatement")) return false;
45393
- for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
45394
- return false;
45682
+ return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
45395
45683
  };
45396
45684
  const returnArgumentUsesAnyName = (returnStatement, names) => {
45397
45685
  if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
@@ -45469,7 +45757,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
45469
45757
  if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
45470
45758
  for (const declarator of stmt.declarations ?? []) {
45471
45759
  const init = declarator.init;
45472
- if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
45760
+ if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes)) {
45473
45761
  memoNode = declarator;
45474
45762
  callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
45475
45763
  if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
@@ -45535,8 +45823,11 @@ const collectFromObjectPattern = (pattern, bindings) => {
45535
45823
  const collectDefaultedEmptyBindings = (functionNode) => {
45536
45824
  const bindings = /* @__PURE__ */ new Map();
45537
45825
  const params = functionNode.params ?? [];
45538
- for (const param of params) collectFromObjectPattern(param, bindings);
45539
- const propsParam = params[0];
45826
+ for (const [parameterIndex, param] of params.entries()) {
45827
+ const parameterBinding = parameterIndex === 0 ? resolveFirstArgumentBinding(param) : param;
45828
+ if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
45829
+ }
45830
+ const propsParam = resolveFirstArgumentBinding(params[0]);
45540
45831
  const body = functionNode.body;
45541
45832
  if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
45542
45833
  if (!isNodeOfType(body, "BlockStatement")) return bindings;
@@ -52963,7 +53254,8 @@ const serverCacheWithObjectLiteral = defineRule({
52963
53254
  if (!isNodeOfType(node.callee, "Identifier")) return;
52964
53255
  if (!cachedFunctionNames.has(node.callee.name)) return;
52965
53256
  const firstArg = node.arguments?.[0];
52966
- 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;
52967
53259
  context.report({
52968
53260
  node,
52969
53261
  message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
@@ -53650,10 +53942,6 @@ const isInvalidStyleExpression = (expression) => {
53650
53942
  }
53651
53943
  return false;
53652
53944
  };
53653
- const getJsxOpeningElementName = (node) => {
53654
- if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
53655
- return null;
53656
- };
53657
53945
  const stylePropObject = defineRule({
53658
53946
  id: "style-prop-object",
53659
53947
  title: "Style prop is not an object",
@@ -53665,7 +53953,8 @@ const stylePropObject = defineRule({
53665
53953
  const allowSet = new Set(allow);
53666
53954
  return {
53667
53955
  JSXOpeningElement(node) {
53668
- const elementName = getJsxOpeningElementName(node);
53956
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
53957
+ const elementName = resolveJsxElementType(node);
53669
53958
  if (elementName && allowSet.has(elementName)) return;
53670
53959
  if (elementName) {
53671
53960
  const firstCharCode = elementName.charCodeAt(0);
@@ -54370,7 +54659,7 @@ const tanstackStartNoAnchorElement = defineRule({
54370
54659
  create: (context) => {
54371
54660
  if (!isInProjectDirectory(context, "routes")) return {};
54372
54661
  return { JSXOpeningElement(node) {
54373
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
54662
+ if (resolveJsxElementType(node) !== "a") return;
54374
54663
  const attributes = node.attributes ?? [];
54375
54664
  const hrefAttribute = findJsxAttribute(attributes, "href");
54376
54665
  if (!hrefAttribute?.value) return;
@@ -54991,8 +55280,7 @@ const voidDomElementsNoChildren = defineRule({
54991
55280
  create: (context) => ({
54992
55281
  JSXElement(node) {
54993
55282
  const openingElement = node.openingElement;
54994
- if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
54995
- const tagName = openingElement.name.name;
55283
+ const tagName = resolveJsxElementType(openingElement);
54996
55284
  if (!VOID_DOM_ELEMENTS.has(tagName)) return;
54997
55285
  const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
54998
55286
  const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.6-dev.e026a23",
3
+ "version": "0.7.6-dev.e141d90",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",