oxlint-plugin-react-doctor 0.7.6-dev.dfd47a7 → 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 +497 -163
  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"));
@@ -13653,6 +13813,28 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
13653
13813
  } })
13654
13814
  });
13655
13815
  //#endregion
13816
+ //#region src/plugin/utils/unwrap-discarded-expression.ts
13817
+ const unwrapDiscardedExpression = (node) => {
13818
+ let expression = isNodeOfType(node, "ExpressionStatement") ? node.expression : node;
13819
+ for (;;) {
13820
+ expression = stripParenExpression(expression);
13821
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") {
13822
+ expression = expression.argument;
13823
+ continue;
13824
+ }
13825
+ if (isNodeOfType(expression, "SequenceExpression")) {
13826
+ const expressions = expression.expressions ?? [];
13827
+ const finalExpression = expressions.at(-1);
13828
+ const prefixExpressions = expressions.slice(0, -1);
13829
+ if (finalExpression && prefixExpressions.length > 0 && prefixExpressions.every((prefixExpression) => isNodeOfType(stripParenExpression(prefixExpression), "Literal"))) {
13830
+ expression = finalExpression;
13831
+ continue;
13832
+ }
13833
+ }
13834
+ return expression;
13835
+ }
13836
+ };
13837
+ //#endregion
13656
13838
  //#region src/plugin/rules/js-performance/js-batch-dom-css.ts
13657
13839
  const ITERATOR_METHOD_NAMES$2 = new Set([
13658
13840
  "forEach",
@@ -13837,9 +14019,19 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
13837
14019
  });
13838
14020
  return foundAttachment;
13839
14021
  };
14022
+ const getStyleAssignment = (node) => {
14023
+ if (!isNodeOfType(node, "ExpressionStatement")) return null;
14024
+ const expression = unwrapDiscardedExpression(node);
14025
+ if (!isNodeOfType(expression, "AssignmentExpression")) return null;
14026
+ if (!isNodeOfType(expression.left, "MemberExpression")) return null;
14027
+ if (!isNodeOfType(expression.left.object, "MemberExpression")) return null;
14028
+ if (!isNodeOfType(expression.left.object.property, "Identifier")) return null;
14029
+ return expression.left.object.property.name === "style" ? expression : null;
14030
+ };
13840
14031
  const isProvablyDetachedAtWrite = (styleWriteStatement) => {
13841
- if (!isNodeOfType(styleWriteStatement, "ExpressionStatement") || !isNodeOfType(styleWriteStatement.expression, "AssignmentExpression") || !isNodeOfType(styleWriteStatement.expression.left, "MemberExpression") || !isNodeOfType(styleWriteStatement.expression.left.object, "MemberExpression")) return false;
13842
- const elementExpression = styleWriteStatement.expression.left.object.object;
14032
+ const assignment = getStyleAssignment(styleWriteStatement);
14033
+ if (!assignment || !isNodeOfType(assignment.left, "MemberExpression") || !isNodeOfType(assignment.left.object, "MemberExpression")) return false;
14034
+ const elementExpression = assignment.left.object.object;
13843
14035
  const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
13844
14036
  if (!creationRoot) return false;
13845
14037
  return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
@@ -13851,15 +14043,17 @@ const jsBatchDomCss = defineRule({
13851
14043
  severity: "warn",
13852
14044
  recommendation: "Do all your reads first, then all your writes. Mixing them inside a loop makes the browser recalculate the layout again and again, which is slow",
13853
14045
  create: (context) => {
13854
- const isStyleAssignment = (node) => isNodeOfType(node, "ExpressionStatement") && isNodeOfType(node.expression, "AssignmentExpression") && isNodeOfType(node.expression.left, "MemberExpression") && isNodeOfType(node.expression.left.object, "MemberExpression") && isNodeOfType(node.expression.left.object.property, "Identifier") && node.expression.left.object.property.name === "style";
13855
- const writesLayoutAffectingProperty = (node) => isNodeOfType(node, "ExpressionStatement") && isNodeOfType(node.expression, "AssignmentExpression") && isNodeOfType(node.expression.left, "MemberExpression") && isNodeOfType(node.expression.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(node.expression.left.property.name);
14046
+ const writesLayoutAffectingProperty = (node) => {
14047
+ const assignment = getStyleAssignment(node);
14048
+ return assignment !== null && isNodeOfType(assignment.left, "MemberExpression") && isNodeOfType(assignment.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(assignment.left.property.name);
14049
+ };
13856
14050
  return { BlockStatement(node) {
13857
14051
  const perIterationBody = findEnclosingPerIterationBody(node);
13858
14052
  if (!perIterationBody) return;
13859
14053
  const statements = node.body ?? [];
13860
14054
  let layoutReads = null;
13861
14055
  for (let statementIndex = 1; statementIndex < statements.length; statementIndex++) {
13862
- if (!isStyleAssignment(statements[statementIndex]) || !isStyleAssignment(statements[statementIndex - 1])) continue;
14056
+ if (getStyleAssignment(statements[statementIndex]) === null || getStyleAssignment(statements[statementIndex - 1]) === null) continue;
13863
14057
  if (!writesLayoutAffectingProperty(statements[statementIndex]) && !writesLayoutAffectingProperty(statements[statementIndex - 1])) continue;
13864
14058
  layoutReads ??= scanPerIterationLayoutReads(perIterationBody);
13865
14059
  if (!layoutReads.hasUsedLayoutRead || layoutReads.hasDeliberateForcedReflow) return;
@@ -14555,6 +14749,18 @@ const jsHoistRegexp = defineRule({
14555
14749
  }, { treatIteratorCallbacksAsLoops: true })
14556
14750
  });
14557
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
14558
14764
  //#region src/plugin/rules/js-performance/js-index-maps.ts
14559
14765
  const referencesParameter = (expression, parameterName) => {
14560
14766
  if (!expression) return false;
@@ -14565,7 +14771,7 @@ const referencesParameter = (expression, parameterName) => {
14565
14771
  const isSingleFieldEqualityPredicate = (node) => {
14566
14772
  const callback = node.arguments?.[0];
14567
14773
  if (!isInlineFunctionExpression(callback)) return false;
14568
- const firstParameter = callback.params?.[0];
14774
+ const firstParameter = resolveFirstArgumentBinding(callback.params?.[0]);
14569
14775
  if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
14570
14776
  let predicate = null;
14571
14777
  const body = callback.body;
@@ -15257,9 +15463,21 @@ const isSmallFixedListMember = (receiver) => {
15257
15463
  };
15258
15464
  const getResolvedInitializer = (receiver) => {
15259
15465
  if (!isNodeOfType(receiver, "Identifier")) return null;
15260
- const initializer = findVariableInitializer(receiver, receiver.name)?.initializer ?? null;
15261
- if (initializer && isNodeOfType(initializer, "Identifier")) return findVariableInitializer(initializer, initializer.name)?.initializer ?? initializer;
15262
- return initializer;
15466
+ const binding = findVariableInitializer(receiver, receiver.name);
15467
+ const initializer = binding?.initializer ?? null;
15468
+ if (!binding || !initializer) return null;
15469
+ const isDefault = isNodeOfType(binding.bindingIdentifier.parent, "AssignmentPattern");
15470
+ if (isNodeOfType(initializer, "Identifier")) {
15471
+ const aliased = findVariableInitializer(initializer, initializer.name);
15472
+ if (aliased?.initializer) return {
15473
+ initializer: aliased.initializer,
15474
+ isDefault: isDefault || isNodeOfType(aliased.bindingIdentifier.parent, "AssignmentPattern")
15475
+ };
15476
+ }
15477
+ return {
15478
+ initializer,
15479
+ isDefault
15480
+ };
15263
15481
  };
15264
15482
  const isSplitCall = (expression) => {
15265
15483
  if (!expression) return false;
@@ -15425,8 +15643,8 @@ const isBoundedConstantCollection = (collection) => {
15425
15643
  if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
15426
15644
  if (isSmallInlineLiteralArray(stripped)) return true;
15427
15645
  if (isNodeOfType(stripped, "Identifier")) {
15428
- const initializer = getResolvedInitializer(stripped);
15429
- if (initializer && isSmallInlineLiteralArray(initializer)) return true;
15646
+ const resolved = getResolvedInitializer(stripped);
15647
+ if (resolved && !resolved.isDefault && isSmallInlineLiteralArray(resolved.initializer)) return true;
15430
15648
  }
15431
15649
  return false;
15432
15650
  };
@@ -15478,8 +15696,8 @@ const jsSetMapLookups = defineRule({
15478
15696
  if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
15479
15697
  const resolvedInitializer = getResolvedInitializer(receiver);
15480
15698
  if (resolvedInitializer) {
15481
- if (isLikelyStringReceiver(resolvedInitializer)) return;
15482
- if (isSmallInlineLiteralArray(resolvedInitializer)) return;
15699
+ if (isLikelyStringReceiver(resolvedInitializer.initializer)) return;
15700
+ if (!resolvedInitializer.isDefault && isSmallInlineLiteralArray(resolvedInitializer.initializer)) return;
15483
15701
  }
15484
15702
  if (isStringElementOfSplitIteration(receiver)) return;
15485
15703
  if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
@@ -15892,14 +16110,21 @@ const jsxFilenameExtension = defineRule({
15892
16110
  });
15893
16111
  //#endregion
15894
16112
  //#region src/plugin/utils/is-jsx-fragment-element.ts
15895
- const isJsxFragmentElement = (node) => {
16113
+ const isJsxFragmentElement = (node, scopes) => {
15896
16114
  if (!isNodeOfType(node, "JSXOpeningElement")) return false;
15897
16115
  const elementName = node.name;
15898
- 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
+ }
15899
16122
  if (isNodeOfType(elementName, "JSXMemberExpression")) {
15900
16123
  if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
15901
- if (elementName.object.name !== "React") return false;
15902
- 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);
15903
16128
  }
15904
16129
  return false;
15905
16130
  };
@@ -15925,7 +16150,7 @@ const jsxFragments = defineRule({
15925
16150
  if (mode !== "syntax") return;
15926
16151
  if (!node.closingElement) return;
15927
16152
  const openingElement = node.openingElement;
15928
- if (!isJsxFragmentElement(openingElement)) return;
16153
+ if (!isJsxFragmentElement(openingElement, context.scopes)) return;
15929
16154
  if (openingElement.attributes.length > 0) return;
15930
16155
  context.report({
15931
16156
  node: openingElement,
@@ -18606,10 +18831,6 @@ const resolveSettings$29 = (settings) => {
18606
18831
  if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
18607
18832
  return reactDoctor.jsxNoScriptUrl ?? {};
18608
18833
  };
18609
- const getElementName = (node) => {
18610
- if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
18611
- return null;
18612
- };
18613
18834
  const isLinkPropForElement = (elementName, attributeName, options) => {
18614
18835
  if (elementName === "a" && attributeName === "href") return true;
18615
18836
  const explicit = options.components?.[elementName];
@@ -18629,7 +18850,8 @@ const jsxNoScriptUrl = defineRule({
18629
18850
  create: (context) => {
18630
18851
  const options = resolveSettings$29(context.settings);
18631
18852
  return { JSXOpeningElement(node) {
18632
- const elementName = getElementName(node);
18853
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
18854
+ const elementName = resolveJsxElementType(node);
18633
18855
  if (!elementName) return;
18634
18856
  for (const attribute of node.attributes) {
18635
18857
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -18816,11 +19038,6 @@ const checkTarget = (attributeValue) => {
18816
19038
  alternate: false
18817
19039
  };
18818
19040
  };
18819
- const getOpeningElementName = (node) => {
18820
- const name = node.name;
18821
- if (isNodeOfType(name, "JSXIdentifier")) return name.name;
18822
- return null;
18823
- };
18824
19041
  const jsxNoTargetBlank = defineRule({
18825
19042
  id: "jsx-no-target-blank",
18826
19043
  title: "Unsafe target=_blank link",
@@ -18840,7 +19057,8 @@ const jsxNoTargetBlank = defineRule({
18840
19057
  return settings.formComponents.has(tagName);
18841
19058
  };
18842
19059
  return { JSXOpeningElement(node) {
18843
- const tagName = getOpeningElementName(node);
19060
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
19061
+ const tagName = resolveJsxElementType(node);
18844
19062
  if (!tagName) return;
18845
19063
  if (!isLink(tagName) && !isForm(tagName)) return;
18846
19064
  const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
@@ -19073,7 +19291,7 @@ const jsxNoUselessFragment = defineRule({
19073
19291
  return {
19074
19292
  JSXElement(node) {
19075
19293
  const openingElement = node.openingElement;
19076
- if (!isJsxFragmentElement(openingElement)) return;
19294
+ if (!isJsxFragmentElement(openingElement, context.scopes)) return;
19077
19295
  if (hasJsxKeyAttribute(openingElement)) return;
19078
19296
  if (checkChildren(node, openingElement, node.children)) return;
19079
19297
  if (isChildOfHtmlElement(node)) context.report({
@@ -20315,11 +20533,24 @@ const hasDirective = (programNode, directive) => {
20315
20533
  return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
20316
20534
  };
20317
20535
  //#endregion
20318
- //#region src/plugin/utils/is-uppercase-name.ts
20319
- const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
20320
- //#endregion
20321
- //#region src/plugin/utils/is-component-assignment.ts
20322
- 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
+ };
20323
20554
  //#endregion
20324
20555
  //#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
20325
20556
  const nextjsAsyncClientComponent = defineRule({
@@ -20345,10 +20576,10 @@ const nextjsAsyncClientComponent = defineRule({
20345
20576
  },
20346
20577
  VariableDeclarator(node) {
20347
20578
  if (!fileHasUseClient) return;
20348
- if (!isComponentAssignment(node)) return;
20349
- if (!isInlineFunctionExpression(node.init)) return;
20350
- if (!node.init.async) return;
20351
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;
20352
20583
  context.report({
20353
20584
  node,
20354
20585
  message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
@@ -20650,7 +20881,7 @@ const nextjsNoAElement = defineRule({
20650
20881
  severity: "warn",
20651
20882
  recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
20652
20883
  create: (context) => ({ JSXOpeningElement(node) {
20653
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
20884
+ if (resolveJsxElementType(node) !== "a") return;
20654
20885
  const attributes = node.attributes ?? [];
20655
20886
  const downloadAttribute = findJsxAttribute(attributes, "download");
20656
20887
  if (downloadAttribute) {
@@ -20846,7 +21077,7 @@ const nextjsNoCssLink = defineRule({
20846
21077
  severity: "warn",
20847
21078
  recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
20848
21079
  create: (context) => ({ JSXOpeningElement(node) {
20849
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
21080
+ if (resolveJsxElementType(node) !== "link") return;
20850
21081
  const attributes = node.attributes ?? [];
20851
21082
  const relAttribute = findJsxAttribute(attributes, "rel");
20852
21083
  if (!relAttribute?.value) return;
@@ -20954,7 +21185,7 @@ const nextjsNoFontLink = defineRule({
20954
21185
  severity: "warn",
20955
21186
  recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
20956
21187
  create: (context) => ({ JSXOpeningElement(node) {
20957
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
21188
+ if (resolveJsxElementType(node) !== "link") return;
20958
21189
  const attributes = node.attributes ?? [];
20959
21190
  const hrefAttribute = findJsxAttribute(attributes, "href");
20960
21191
  if (!hrefAttribute?.value) return;
@@ -21129,7 +21360,7 @@ const nextjsNoImgElement = defineRule({
21129
21360
  create: (context) => {
21130
21361
  if (isGeneratedImageRenderContext(context)) return {};
21131
21362
  return { JSXOpeningElement(node) {
21132
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
21363
+ if (resolveJsxElementType(node) !== "img") return;
21133
21364
  if (isGeneratedImageRenderContext(context, node)) return;
21134
21365
  const programRoot = findProgramRoot(node);
21135
21366
  if (programRoot && hasEmailTemplateImport(programRoot)) return;
@@ -21176,8 +21407,8 @@ const nextjsNoPolyfillScript = defineRule({
21176
21407
  severity: "warn",
21177
21408
  recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
21178
21409
  create: (context) => ({ JSXOpeningElement(node) {
21179
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
21180
- if (node.name.name !== "script" && node.name.name !== "Script") return;
21410
+ const elementName = resolveJsxElementType(node);
21411
+ if (elementName !== "script" && elementName !== "Script") return;
21181
21412
  const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
21182
21413
  if (!srcAttribute?.value) return;
21183
21414
  const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
@@ -22985,10 +23216,15 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
22985
23216
  if (!declaringNode) return false;
22986
23217
  return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
22987
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
+ };
22988
23225
  const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
22989
23226
  if (def.type !== "Parameter") return false;
22990
- const bindingParent = def.name.parent;
22991
- return isFunctionLike$1(bindingParent);
23227
+ return isWholePropsParameterBinding(def.name);
22992
23228
  }));
22993
23229
  const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
22994
23230
  const isPropAlias = (analysis, ref) => {
@@ -25985,6 +26221,56 @@ const noBarrelImport = defineRule({
25985
26221
  }
25986
26222
  });
25987
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
25988
26274
  //#region src/plugin/utils/function-contains-react-render-output.ts
25989
26275
  const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
25990
26276
  "FunctionDeclaration",
@@ -26004,12 +26290,13 @@ const isCallArgumentFunctionExpression = (node) => {
26004
26290
  return parent.arguments.some((argumentNode) => argumentNode === node);
26005
26291
  };
26006
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);
26007
26294
  const containsRenderOutput$1 = (rootNode, scopes) => {
26008
26295
  let hasRenderOutput = false;
26009
26296
  walkAst(rootNode, (node) => {
26010
26297
  if (hasRenderOutput) return false;
26011
26298
  if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
26012
- if (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
26299
+ if (isRenderOutputExpression(node, scopes)) {
26013
26300
  hasRenderOutput = true;
26014
26301
  return false;
26015
26302
  }
@@ -26020,7 +26307,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
26020
26307
  const functionContainsReactRenderOutput = (functionNode, scopes) => {
26021
26308
  const cachedEntry = renderOutputCache.get(functionNode);
26022
26309
  if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
26023
- const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
26310
+ const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
26024
26311
  renderOutputCache.set(functionNode, {
26025
26312
  scopes,
26026
26313
  hasRenderOutput
@@ -26028,6 +26315,9 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
26028
26315
  return hasRenderOutput;
26029
26316
  };
26030
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
26031
26321
  //#region src/plugin/utils/is-component-declaration.ts
26032
26322
  const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
26033
26323
  //#endregion
@@ -28485,7 +28775,7 @@ const noDisabledZoom = defineRule({
28485
28775
  category: "Accessibility",
28486
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.",
28487
28777
  create: (context) => ({ JSXOpeningElement(node) {
28488
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
28778
+ if (resolveJsxElementType(node) !== "meta") return;
28489
28779
  const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
28490
28780
  if (!nameAttr?.value) return;
28491
28781
  if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
@@ -28875,7 +29165,7 @@ const findTopLevelEffectCalls = (componentBody) => {
28875
29165
  if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
28876
29166
  for (const statement of componentBody.body ?? []) {
28877
29167
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
28878
- const expression = statement.expression;
29168
+ const expression = unwrapDiscardedExpression(statement);
28879
29169
  if (!isNodeOfType(expression, "CallExpression")) continue;
28880
29170
  if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
28881
29171
  effectCalls.push(expression);
@@ -31657,7 +31947,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
31657
31947
  severity: "warn",
31658
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.",
31659
31949
  create: (context) => ({ JSXOpeningElement(node) {
31660
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
31950
+ if (resolveJsxElementType(node) !== "img") return;
31661
31951
  const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
31662
31952
  if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
31663
31953
  const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
@@ -31898,7 +32188,7 @@ const noIndeterminateAttribute = defineRule({
31898
32188
  if (classifyReactNativeFileTarget(context) === "react-native") return {};
31899
32189
  return {
31900
32190
  JSXOpeningElement(node) {
31901
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
32191
+ if (resolveJsxElementType(node) !== "input") return;
31902
32192
  let typeAttribute = null;
31903
32193
  let typeAttributeIndex = null;
31904
32194
  let indeterminateAttribute = null;
@@ -32077,11 +32367,12 @@ const isMemoCall = (node) => {
32077
32367
  };
32078
32368
  const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
32079
32369
  const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
32080
- const isInlineReference = (node) => {
32081
- 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";
32082
- if (isNodeOfType(node, "ObjectExpression")) return "objects";
32083
- if (isNodeOfType(node, "ArrayExpression")) return "Arrays";
32084
- 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";
32085
32376
  return null;
32086
32377
  };
32087
32378
  const noInlinePropOnMemoComponent = defineRule({
@@ -32111,7 +32402,7 @@ const noInlinePropOnMemoComponent = defineRule({
32111
32402
  let elementName = null;
32112
32403
  if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
32113
32404
  if (!elementName || !memoizedComponentNames.has(elementName)) return;
32114
- const propType = isInlineReference(node.value.expression);
32405
+ const propType = isInlineReference(node.value.expression, context.scopes);
32115
32406
  if (propType) context.report({
32116
32407
  node: node.value.expression,
32117
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`
@@ -32970,11 +33261,13 @@ const noManyBooleanProps = defineRule({
32970
33261
  };
32971
33262
  const checkComponent = (functionNode, param, body, componentName, reportNode) => {
32972
33263
  if (!param) return;
33264
+ const propsBinding = resolveFirstArgumentBinding(param);
33265
+ if (!propsBinding) return;
32973
33266
  if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
32974
- if (isNodeOfType(param, "ObjectPattern")) {
33267
+ if (isNodeOfType(propsBinding, "ObjectPattern")) {
32975
33268
  const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
32976
33269
  const booleanLikePropNames = [];
32977
- for (const property of param.properties ?? []) {
33270
+ for (const property of propsBinding.properties ?? []) {
32978
33271
  if (!isNodeOfType(property, "Property")) continue;
32979
33272
  const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
32980
33273
  if (!keyName) continue;
@@ -32985,7 +33278,7 @@ const noManyBooleanProps = defineRule({
32985
33278
  reportIfMany(booleanLikePropNames, componentName, reportNode);
32986
33279
  return;
32987
33280
  }
32988
- if (isNodeOfType(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
33281
+ if (isNodeOfType(propsBinding, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, propsBinding.name)], componentName, reportNode);
32989
33282
  };
32990
33283
  return {
32991
33284
  FunctionDeclaration(node) {
@@ -33107,7 +33400,7 @@ const noMirrorPropEffect = defineRule({
33107
33400
  if (mirrorBindings.length === 0) return;
33108
33401
  for (const statement of componentBody.body ?? []) {
33109
33402
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
33110
- const effectCall = statement.expression;
33403
+ const effectCall = unwrapDiscardedExpression(statement);
33111
33404
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
33112
33405
  if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
33113
33406
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
@@ -33121,7 +33414,7 @@ const noMirrorPropEffect = defineRule({
33121
33414
  const bodyStatements = getCallbackStatements(callback);
33122
33415
  if (bodyStatements.length !== 1) continue;
33123
33416
  const onlyStatement = bodyStatements[0];
33124
- const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
33417
+ const expression = unwrapDiscardedExpression(onlyStatement);
33125
33418
  if (!isNodeOfType(expression, "CallExpression")) continue;
33126
33419
  if (!isNodeOfType(expression.callee, "Identifier")) continue;
33127
33420
  if (!isSetterIdentifier(expression.callee.name)) continue;
@@ -35737,7 +36030,7 @@ const noPreventDefault = defineRule({
35737
36030
  const isServerActionsFramework = hasCapability(context.settings, "server-actions");
35738
36031
  const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
35739
36032
  return { JSXOpeningElement(node) {
35740
- const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
36033
+ const elementName = resolveJsxElementType(node);
35741
36034
  if (!elementName) return;
35742
36035
  const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
35743
36036
  if (!targetEventProps) return;
@@ -37222,7 +37515,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
37222
37515
  return expressionReadsStateValue(argument, stateName);
37223
37516
  };
37224
37517
  const getUnconditionalSetterCall = (statement, setterNames) => {
37225
- const expression = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
37518
+ const expression = unwrapDiscardedExpression(statement);
37226
37519
  if (!isNodeOfType(expression, "CallExpression")) return null;
37227
37520
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
37228
37521
  if (!setterNames.has(expression.callee.name)) return null;
@@ -37571,7 +37864,7 @@ const noSelfUpdatingEffect = defineRule({
37571
37864
  const setterNames = new Set(setterNameToStateName.keys());
37572
37865
  for (const statement of functionBody.body ?? []) {
37573
37866
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
37574
- const effectCall = statement.expression;
37867
+ const effectCall = unwrapDiscardedExpression(statement);
37575
37868
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
37576
37869
  if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
37577
37870
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
@@ -38201,9 +38494,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
38201
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.",
38202
38495
  create: (context) => ({ JSXOpeningElement(node) {
38203
38496
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
38204
- const firstCharacter = node.name.name.charCodeAt(0);
38497
+ const elementName = resolveJsxElementType(node);
38498
+ const firstCharacter = elementName.charCodeAt(0);
38205
38499
  if (firstCharacter < 97 || firstCharacter > 122) return;
38206
- if (node.name.name.includes("-")) return;
38500
+ if (elementName.includes("-")) return;
38207
38501
  for (const attribute of node.attributes) {
38208
38502
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
38209
38503
  if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
@@ -38591,8 +38885,7 @@ const noUncontrolledInput = defineRule({
38591
38885
  const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
38592
38886
  walkAst(componentBody, (child) => {
38593
38887
  if (!isNodeOfType(child, "JSXOpeningElement")) return;
38594
- if (!isNodeOfType(child.name, "JSXIdentifier")) return;
38595
- const tagName = child.name.name;
38888
+ const tagName = resolveJsxElementType(child);
38596
38889
  if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
38597
38890
  const attributes = child.attributes ?? [];
38598
38891
  if (hasJsxSpreadAttribute(attributes)) return;
@@ -38653,7 +38946,7 @@ const noUndeferredThirdParty = defineRule({
38653
38946
  severity: "warn",
38654
38947
  recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
38655
38948
  create: (context) => ({ JSXOpeningElement(node) {
38656
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
38949
+ if (resolveJsxElementType(node) !== "script") return;
38657
38950
  const attributes = node.attributes ?? [];
38658
38951
  const srcAttribute = findJsxAttribute(attributes, "src");
38659
38952
  if (!srcAttribute) return;
@@ -39868,7 +40161,7 @@ const noUnknownProperty = defineRule({
39868
40161
  }
39869
40162
  if (fileIsNonReactJsx) return;
39870
40163
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
39871
- const elementType = node.name.name;
40164
+ const elementType = resolveJsxElementType(node);
39872
40165
  const firstCharacter = elementType.charCodeAt(0);
39873
40166
  if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
39874
40167
  let isValidHtmlTag = isKnownDomTag(elementType);
@@ -40046,20 +40339,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
40046
40339
  });
40047
40340
  return found;
40048
40341
  };
40342
+ const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
40049
40343
  const isReactClassComponent = (classNode) => {
40050
40344
  if (isEs6Component(classNode)) return true;
40051
40345
  return expressionContainsJsxOrCreateElement(classNode);
40052
40346
  };
40053
- const findEnclosingComponent = (node) => {
40347
+ const findEnclosingComponent = (node, scopes) => {
40054
40348
  let walker = node.parent;
40055
40349
  while (walker) {
40056
40350
  if (isFunctionLike$1(walker)) {
40057
40351
  const componentName = inferFunctionLikeName(walker);
40058
- if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
40352
+ if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
40059
40353
  component: walker,
40060
40354
  name: componentName
40061
40355
  };
40062
- if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
40356
+ if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
40063
40357
  component: walker,
40064
40358
  name: null
40065
40359
  };
@@ -40298,7 +40592,7 @@ const noUnstableNestedComponents = defineRule({
40298
40592
  if (renderPropRegex.test(propInfo.propName)) return;
40299
40593
  if (settings.allowAsProps) return;
40300
40594
  }
40301
- const enclosing = findEnclosingComponent(candidateNode);
40595
+ const enclosing = findEnclosingComponent(candidateNode, context.scopes);
40302
40596
  if (!enclosing) return;
40303
40597
  const gatedName = propInfo ? null : requiredInstantiationName;
40304
40598
  queuedReports.push({
@@ -40309,7 +40603,7 @@ const noUnstableNestedComponents = defineRule({
40309
40603
  });
40310
40604
  };
40311
40605
  const checkFunctionLike = (node) => {
40312
- if (!expressionContainsJsxOrCreateElement(node)) return;
40606
+ if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
40313
40607
  const inferredName = inferFunctionLikeName(node);
40314
40608
  const propInfo = isComponentDeclaredInProp(node);
40315
40609
  const isObjectCallback = isObjectCallbackCandidate(node);
@@ -41569,7 +41863,7 @@ const preactPreferOndblclick = defineRule({
41569
41863
  recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
41570
41864
  create: (context) => ({ JSXOpeningElement(node) {
41571
41865
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
41572
- const tagName = node.name.name;
41866
+ const tagName = resolveJsxElementType(node);
41573
41867
  if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
41574
41868
  const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
41575
41869
  if (!onDoubleClickAttribute) return;
@@ -41589,7 +41883,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
41589
41883
  ]);
41590
41884
  const isTextLikeInput = (openingElement) => {
41591
41885
  if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
41592
- const tagName = openingElement.name.name;
41886
+ const tagName = resolveJsxElementType(openingElement);
41593
41887
  if (tagName === "textarea") return true;
41594
41888
  if (tagName !== "input") return false;
41595
41889
  const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
@@ -41746,8 +42040,9 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
41746
42040
  ]);
41747
42041
  const collectBooleanPropBindings = (param) => {
41748
42042
  const bindings = /* @__PURE__ */ new Set();
41749
- if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
41750
- 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 ?? []) {
41751
42046
  if (!isNodeOfType(property, "Property")) continue;
41752
42047
  if (property.computed) continue;
41753
42048
  if (!isNodeOfType(property.key, "Identifier")) continue;
@@ -42008,7 +42303,7 @@ const preferHtmlDialog = defineRule({
42008
42303
  },
42009
42304
  JSXOpeningElement(node) {
42010
42305
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
42011
- const tagName = node.name.name;
42306
+ const tagName = resolveJsxElementType(node);
42012
42307
  if (tagName === "dialog") return;
42013
42308
  if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
42014
42309
  if (!HTML_TAGS.has(tagName)) return;
@@ -43555,13 +43850,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
43555
43850
  }
43556
43851
  return null;
43557
43852
  };
43558
- const resolveHookNameFromInitializer = (initializer) => {
43853
+ const resolveHookNameFromInitializer = (initializer, scopes) => {
43559
43854
  if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
43560
43855
  if (!isNodeOfType(initializer, "Identifier")) return null;
43561
- const binding = findVariableInitializer(initializer, initializer.name);
43562
- if (!binding?.initializer || !isConstDeclaredBinding(binding)) return null;
43563
- if (!isNodeOfType(binding.initializer, "CallExpression")) return null;
43564
- 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);
43565
43860
  };
43566
43861
  const queryNoRestDestructuring = defineRule({
43567
43862
  id: "query-no-rest-destructuring",
@@ -43574,7 +43869,7 @@ const queryNoRestDestructuring = defineRule({
43574
43869
  if (!isNodeOfType(node.id, "ObjectPattern")) return;
43575
43870
  if (!node.init) return;
43576
43871
  if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
43577
- const hookName = resolveHookNameFromInitializer(node.init);
43872
+ const hookName = resolveHookNameFromInitializer(node.init, context.scopes);
43578
43873
  if (!hookName) return;
43579
43874
  context.report({
43580
43875
  node: node.id,
@@ -44045,7 +44340,7 @@ const renderingAnimateSvgWrapper = defineRule({
44045
44340
  severity: "warn",
44046
44341
  recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
44047
44342
  create: (context) => ({ JSXOpeningElement(node) {
44048
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
44343
+ if (resolveJsxElementType(node) !== "svg") return;
44049
44344
  if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
44050
44345
  node,
44051
44346
  message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
@@ -44929,7 +45224,7 @@ const DEFERRABLE_HOOK_NAMES = new Set([
44929
45224
  "useParams",
44930
45225
  "usePathname"
44931
45226
  ]);
44932
- const findHookCallBindings = (componentBody) => {
45227
+ const findHookCallBindings = (componentBody, scopes) => {
44933
45228
  const bindings = [];
44934
45229
  if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
44935
45230
  for (const statement of componentBody.body ?? []) {
@@ -44940,8 +45235,10 @@ const findHookCallBindings = (componentBody) => {
44940
45235
  const callee = declarator.init.callee;
44941
45236
  if (!isNodeOfType(callee, "Identifier")) continue;
44942
45237
  if (!DEFERRABLE_HOOK_NAMES.has(callee.name)) continue;
45238
+ const valueSymbol = scopes.symbolFor(declarator.id);
45239
+ if (!valueSymbol) continue;
44943
45240
  bindings.push({
44944
- valueName: declarator.id.name,
45241
+ valueSymbol,
44945
45242
  hookName: callee.name,
44946
45243
  declarator
44947
45244
  });
@@ -44949,6 +45246,49 @@ const findHookCallBindings = (componentBody) => {
44949
45246
  }
44950
45247
  return bindings;
44951
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
+ };
44952
45292
  const rerenderDeferReadsHook = defineRule({
44953
45293
  id: "rerender-defer-reads-hook",
44954
45294
  title: "URL hook value only read in handlers",
@@ -44959,15 +45299,13 @@ const rerenderDeferReadsHook = defineRule({
44959
45299
  create: (context) => {
44960
45300
  const checkComponent = (componentBody) => {
44961
45301
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
44962
- const bindings = findHookCallBindings(componentBody);
45302
+ const bindings = findHookCallBindings(componentBody, context.scopes);
44963
45303
  if (bindings.length === 0) return;
44964
45304
  const handlerBindingNames = collectHandlerBindingNames(componentBody);
44965
45305
  for (const binding of bindings) {
45306
+ const { symbols, aliasSourceIdentifiers } = collectExactAliasSymbols(componentBody, binding.valueSymbol, context.scopes);
44966
45307
  const referenceLocations = [];
44967
- walkAst(componentBody, (child) => {
44968
- if (isNodeOfType(binding.declarator, "VariableDeclarator") && child === binding.declarator.id) return;
44969
- if (isNodeOfType(child, "Identifier") && child.name === binding.valueName) referenceLocations.push(child);
44970
- });
45308
+ for (const symbol of symbols) for (const reference of symbol.references) if (!aliasSourceIdentifiers.has(reference.identifier)) referenceLocations.push(reference.identifier);
44971
45309
  if (referenceLocations.length === 0) continue;
44972
45310
  if (!referenceLocations.every((ref) => isInsideEventHandler(ref, handlerBindingNames))) continue;
44973
45311
  context.report({
@@ -45338,14 +45676,10 @@ const rerenderLazyStateInit = defineRule({
45338
45676
  //#endregion
45339
45677
  //#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
45340
45678
  const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
45341
- const callbackReturnsJsx = (callback) => {
45679
+ const callbackReturnsJsx = (callback, scopes) => {
45342
45680
  if (!callback) return false;
45343
45681
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
45344
- const body = callback.body;
45345
- if (isJsxExpression(body)) return true;
45346
- if (!isNodeOfType(body, "BlockStatement")) return false;
45347
- for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
45348
- return false;
45682
+ return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
45349
45683
  };
45350
45684
  const returnArgumentUsesAnyName = (returnStatement, names) => {
45351
45685
  if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
@@ -45423,7 +45757,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
45423
45757
  if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
45424
45758
  for (const declarator of stmt.declarations ?? []) {
45425
45759
  const init = declarator.init;
45426
- 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)) {
45427
45761
  memoNode = declarator;
45428
45762
  callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
45429
45763
  if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
@@ -45489,8 +45823,11 @@ const collectFromObjectPattern = (pattern, bindings) => {
45489
45823
  const collectDefaultedEmptyBindings = (functionNode) => {
45490
45824
  const bindings = /* @__PURE__ */ new Map();
45491
45825
  const params = functionNode.params ?? [];
45492
- for (const param of params) collectFromObjectPattern(param, bindings);
45493
- 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]);
45494
45831
  const body = functionNode.body;
45495
45832
  if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
45496
45833
  if (!isNodeOfType(body, "BlockStatement")) return bindings;
@@ -45996,8 +46333,8 @@ const rnAnimationReactionAsDerived = defineRule({
45996
46333
  if (statements.length !== 1) return;
45997
46334
  const onlyStatement = statements[0];
45998
46335
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
45999
- singleAssignment = onlyStatement.expression;
46000
- } else if (body) singleAssignment = body;
46336
+ singleAssignment = unwrapDiscardedExpression(onlyStatement);
46337
+ } else if (body) singleAssignment = unwrapDiscardedExpression(body);
46001
46338
  if (!singleAssignment) return;
46002
46339
  const isValueAssignment = isNodeOfType(singleAssignment, "AssignmentExpression") && isNodeOfType(singleAssignment.left, "MemberExpression") && isNodeOfType(singleAssignment.left.object, "Identifier") && isNodeOfType(singleAssignment.left.property, "Identifier") && singleAssignment.left.property.name === "value";
46003
46340
  const isSetCall = isNodeOfType(singleAssignment, "CallExpression") && isNodeOfType(singleAssignment.callee, "MemberExpression") && isNodeOfType(singleAssignment.callee.property, "Identifier") && singleAssignment.callee.property.name === "set" && (singleAssignment.arguments?.length ?? 0) === 1;
@@ -52917,7 +53254,8 @@ const serverCacheWithObjectLiteral = defineRule({
52917
53254
  if (!isNodeOfType(node.callee, "Identifier")) return;
52918
53255
  if (!cachedFunctionNames.has(node.callee.name)) return;
52919
53256
  const firstArg = node.arguments?.[0];
52920
- 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;
52921
53259
  context.report({
52922
53260
  node,
52923
53261
  message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
@@ -53604,10 +53942,6 @@ const isInvalidStyleExpression = (expression) => {
53604
53942
  }
53605
53943
  return false;
53606
53944
  };
53607
- const getJsxOpeningElementName = (node) => {
53608
- if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
53609
- return null;
53610
- };
53611
53945
  const stylePropObject = defineRule({
53612
53946
  id: "style-prop-object",
53613
53947
  title: "Style prop is not an object",
@@ -53619,7 +53953,8 @@ const stylePropObject = defineRule({
53619
53953
  const allowSet = new Set(allow);
53620
53954
  return {
53621
53955
  JSXOpeningElement(node) {
53622
- const elementName = getJsxOpeningElementName(node);
53956
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
53957
+ const elementName = resolveJsxElementType(node);
53623
53958
  if (elementName && allowSet.has(elementName)) return;
53624
53959
  if (elementName) {
53625
53960
  const firstCharCode = elementName.charCodeAt(0);
@@ -54324,7 +54659,7 @@ const tanstackStartNoAnchorElement = defineRule({
54324
54659
  create: (context) => {
54325
54660
  if (!isInProjectDirectory(context, "routes")) return {};
54326
54661
  return { JSXOpeningElement(node) {
54327
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
54662
+ if (resolveJsxElementType(node) !== "a") return;
54328
54663
  const attributes = node.attributes ?? [];
54329
54664
  const hrefAttribute = findJsxAttribute(attributes, "href");
54330
54665
  if (!hrefAttribute?.value) return;
@@ -54945,8 +55280,7 @@ const voidDomElementsNoChildren = defineRule({
54945
55280
  create: (context) => ({
54946
55281
  JSXElement(node) {
54947
55282
  const openingElement = node.openingElement;
54948
- if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
54949
- const tagName = openingElement.name.name;
55283
+ const tagName = resolveJsxElementType(openingElement);
54950
55284
  if (!VOID_DOM_ELEMENTS.has(tagName)) return;
54951
55285
  const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
54952
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.dfd47a7",
3
+ "version": "0.7.6-dev.e141d90",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",