oxlint-plugin-react-doctor 0.7.5-dev.dbd4067 → 0.7.6-dev.0150fc9

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 +719 -214
  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
@@ -6929,6 +7093,30 @@ const corsCookieTrustRisk = defineRule({
6929
7093
  })
6930
7094
  });
6931
7095
  //#endregion
7096
+ //#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
7097
+ const findMatchingBracket = (content, openIndex) => {
7098
+ const open = content[openIndex];
7099
+ const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
7100
+ if (close === "") return -1;
7101
+ let depth = 0;
7102
+ let stringDelimiter = null;
7103
+ for (let index = openIndex; index < content.length; index += 1) {
7104
+ const character = content[index];
7105
+ if (stringDelimiter !== null) {
7106
+ if (character === "\\") index += 1;
7107
+ else if (character === stringDelimiter) stringDelimiter = null;
7108
+ continue;
7109
+ }
7110
+ if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
7111
+ else if (character === open) depth += 1;
7112
+ else if (character === close) {
7113
+ depth -= 1;
7114
+ if (depth === 0) return index;
7115
+ }
7116
+ }
7117
+ return -1;
7118
+ };
7119
+ //#endregion
6932
7120
  //#region src/plugin/rules/security-scan/dangerous-html-sink.ts
6933
7121
  const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
6934
7122
  const HTML_VALUE_START_PATTERN = /(?:__html\s*:|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(\s*[^,]*,|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\()\s*([\s\S]*)/;
@@ -6944,6 +7132,7 @@ const I18N_VALUE_PATTERN = /\b(?:t|i18n|translate|formatMessage|intl)\s*[.(]/;
6944
7132
  const ESCAPING_SERIALIZER_CALL_PATTERN = /^(?:[\w$.]+\.)?(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*)\s*\(/;
6945
7133
  const HIGHLIGHTER_LIBRARY_PATTERN = /\b(?:shiki|prism|hljs|highlightjs|getHighlighter|codeToHtml|codeToHast|refractor|lowlight|starry-night)\b|highlight\.js/i;
6946
7134
  const SERIALIZER_ASSIGNMENT_PATTERN = /[:=]\s*[^\n;]*(?:\b(?:katex|shiki|hljs|prism|mermaid)\b|hast-util-to-html|renderHtmlFromRichText|(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast)\s*\()/i;
7135
+ const SERIALIZER_CALL_PROVENANCE_PATTERN = /\b(?:(?:katex|shiki|hljs|prism|mermaid|highlighter)[\w$]*\.(?:render\w*|highlight\w*|codeTo(?:Html|Hast))|(?:toHtml|render(?:Html|HTML)[A-Za-z]*|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*))\s*\(/i;
6947
7136
  const BARE_IDENTIFIER_VALUE_PATTERN = /^[\w$]+\s*(?:[;,})\n]|$)/;
6948
7137
  const IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN = /^[\w$]+\s*(?:\|\||\?\?)\s*(?:"[^"\n]*"|'[^'\n]*'|`[^`$]*`)\s*(?:[;,})\n]|$)/;
6949
7138
  const MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN = /^[\w$]+(?:\.[\w$]+|\[[^\]]*\])+\s*(?:[;,})\n]|$)/;
@@ -7103,22 +7292,66 @@ const findContainingBlockEndIndex = (fileContent, targetIndex) => {
7103
7292
  return openingBraceIndex === void 0 ? fileContent.length : findMatchingBraceIndex(fileContent, openingBraceIndex);
7104
7293
  };
7105
7294
  const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) => {
7106
- const initializerPattern = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
7295
+ const initializerPattern = new RegExp(`(const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
7107
7296
  let nearestDeclaration = null;
7108
7297
  let nearestDeclarationIndex = -1;
7109
7298
  for (const match of fileContent.matchAll(initializerPattern)) {
7110
7299
  const declarationIndex = match.index;
7111
7300
  if (declarationIndex === void 0 || declarationIndex >= sinkIndex || declarationIndex <= nearestDeclarationIndex || findContainingBlockEndIndex(fileContent, declarationIndex) < sinkIndex) continue;
7112
7301
  nearestDeclarationIndex = declarationIndex;
7113
- const initializer = match[1];
7302
+ const initializer = match[2];
7114
7303
  if (initializer === void 0) continue;
7115
7304
  nearestDeclaration = {
7305
+ declarationIndex,
7116
7306
  initializer,
7117
- initializerStartIndex: declarationIndex + match[0].length - initializer.length
7307
+ initializerStartIndex: declarationIndex + match[0].length - initializer.length,
7308
+ isImmutable: match[1] === "const"
7118
7309
  };
7119
7310
  }
7120
7311
  return nearestDeclaration;
7121
7312
  };
7313
+ const isDeclarationStable = (identifier, declaration, usageIndex, fileContent) => {
7314
+ if (declaration.isImmutable) return true;
7315
+ const textAfterInitializer = fileContent.slice(declaration.initializerStartIndex + declaration.initializer.length, usageIndex);
7316
+ return !new RegExp(`(?:^|[^\\w$.])${escapeRegExp(identifier)}\\s*=(?!=)`).test(textAfterInitializer);
7317
+ };
7318
+ const isTrustedHighlighterValue = (valueExpression, fileContent, sinkIndex) => {
7319
+ if (!/highlight/i.test(valueExpression)) return false;
7320
+ const identifier = valueExpression.match(/^([\w$]+)/)?.[1];
7321
+ if (identifier === void 0) return false;
7322
+ const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
7323
+ if (declaration !== null) return isDeclarationStable(identifier, declaration, sinkIndex, fileContent) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(declaration.initializer);
7324
+ return /highlighted/i.test(valueExpression) || HIGHLIGHTER_LIBRARY_PATTERN.test(fileContent);
7325
+ };
7326
+ const isExplicitlyTrustedHtmlValue = (valueExpression, fileContent, sinkIndex, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
7327
+ const trimmedExpression = valueExpression.trim();
7328
+ const concatenatedParts = splitTopLevelByPlus(trimmedExpression);
7329
+ if (concatenatedParts.length > 1) return concatenatedParts.every((part) => isExplicitlyTrustedHtmlValue(part, fileContent, sinkIndex, visitedIdentifiers));
7330
+ const interpolationParts = [...trimmedExpression.matchAll(/\$\{([^}]*)\}/g)].flatMap((match) => match[1] === void 0 ? [] : [match[1]]);
7331
+ if (interpolationParts.length > 1) return interpolationParts.every((part) => isExplicitlyTrustedHtmlValue(part, fileContent, sinkIndex, visitedIdentifiers));
7332
+ const identifier = trimmedExpression.match(/^([\w$]+)(?:(?:\??\.[\w$]+)|(?:\[\s*(?:"[^"\n]*"|'[^'\n]*'|\d+)\s*\]))*$/)?.[1];
7333
+ if (identifier && !visitedIdentifiers.has(identifier)) {
7334
+ const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
7335
+ if (declaration && isDeclarationStable(identifier, declaration, sinkIndex, fileContent)) {
7336
+ const nextVisitedIdentifiers = new Set(visitedIdentifiers);
7337
+ nextVisitedIdentifiers.add(identifier);
7338
+ return isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex, nextVisitedIdentifiers);
7339
+ }
7340
+ }
7341
+ if (STRING_LITERAL_VALUE_PATTERN.test(`${trimmedExpression};`) || MODULE_CONSTANT_VALUE_PATTERN.test(`${trimmedExpression};`) || SANITIZER_PATTERN.test(trimmedExpression) || ENV_CONFIG_VALUE_PATTERN.test(trimmedExpression) || I18N_VALUE_PATTERN.test(trimmedExpression) || ESCAPING_SERIALIZER_CALL_PATTERN.test(trimmedExpression) || isTrustedHighlighterValue(trimmedExpression, fileContent, sinkIndex)) return true;
7342
+ return false;
7343
+ };
7344
+ const doesExpressionAliasIdentifier = (expression, targetIdentifier, expressionIndex, fileContent, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
7345
+ const identifier = expression.trim().match(/^([\w$]+)$/)?.[1];
7346
+ if (!identifier) return false;
7347
+ if (identifier === targetIdentifier) return true;
7348
+ if (visitedIdentifiers.has(identifier)) return false;
7349
+ const declaration = findVisibleIdentifierDeclaration(identifier, expressionIndex, fileContent);
7350
+ if (!declaration?.isImmutable) return false;
7351
+ const nextVisitedIdentifiers = new Set(visitedIdentifiers);
7352
+ nextVisitedIdentifiers.add(identifier);
7353
+ return doesExpressionAliasIdentifier(declaration.initializer, targetIdentifier, declaration.initializerStartIndex, fileContent, nextVisitedIdentifiers);
7354
+ };
7122
7355
  const findContainingFunctionParameterSource = (identifier, sinkIndex, fileContent) => {
7123
7356
  const patterns = [/function\s+([\w$]+)\s*\(([^)]*)\)\s*\{/g, /(?:const|let|var)\s+([\w$]+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>\s*\{/g];
7124
7357
  let closestSource = null;
@@ -7126,12 +7359,16 @@ const findContainingFunctionParameterSource = (identifier, sinkIndex, fileConten
7126
7359
  for (const pattern of patterns) for (const match of fileContent.matchAll(pattern)) {
7127
7360
  const matchIndex = match.index;
7128
7361
  if (matchIndex === void 0 || matchIndex >= sinkIndex || matchIndex < closestStartIndex) continue;
7129
- if (findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{")) < sinkIndex) continue;
7362
+ const bodyEndIndex = findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{"));
7363
+ if (bodyEndIndex < sinkIndex) continue;
7130
7364
  const parameterIndex = (match[2] ?? "").split(",").findIndex((parameter) => parameter.trim().match(/^(?:\.\.\.)?([\w$]+)/)?.[1] === identifier);
7131
7365
  if (parameterIndex < 0) continue;
7132
7366
  closestStartIndex = matchIndex;
7367
+ const functionName = match[1] ?? "";
7133
7368
  closestSource = {
7134
- functionName: match[1] ?? "",
7369
+ bodyEndIndex,
7370
+ declarationNameIndex: matchIndex + match[0].indexOf(functionName),
7371
+ functionName,
7135
7372
  parameterIndex
7136
7373
  };
7137
7374
  }
@@ -7162,28 +7399,56 @@ const splitTopLevelArguments = (argumentText) => {
7162
7399
  argumentsList.push(argumentText.slice(startIndex).trim());
7163
7400
  return argumentsList;
7164
7401
  };
7165
- const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers) => {
7166
- const trimmedExpression = expression.trim();
7167
- if (STRING_LITERAL_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
7168
- if (MODULE_CONSTANT_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
7169
- if (SANITIZER_PATTERN.test(trimmedExpression)) return false;
7170
- if (ENV_CONFIG_VALUE_PATTERN.test(trimmedExpression)) return false;
7171
- if (I18N_VALUE_PATTERN.test(trimmedExpression)) return false;
7172
- if (ESCAPING_SERIALIZER_CALL_PATTERN.test(trimmedExpression)) return false;
7173
- if (HTML_TAINT_PATTERN.test(trimmedExpression)) return true;
7174
- const identifier = trimmedExpression.match(/^([\w$]+)\s*(?:[;,})\n]|$)/)?.[1];
7402
+ const isBackedByFunctionParameter = (expression, expressionIndex, fileContent, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
7403
+ const identifier = expression.trim().match(/^([\w$]+)$/)?.[1];
7175
7404
  if (identifier === void 0 || visitedIdentifiers.has(identifier)) return false;
7405
+ if (findContainingFunctionParameterSource(identifier, expressionIndex, fileContent) !== null) return true;
7406
+ const declaration = findVisibleIdentifierDeclaration(identifier, expressionIndex, fileContent);
7407
+ if (!declaration || !isDeclarationStable(identifier, declaration, expressionIndex, fileContent)) return false;
7408
+ const nextVisitedIdentifiers = new Set(visitedIdentifiers);
7409
+ nextVisitedIdentifiers.add(identifier);
7410
+ return isBackedByFunctionParameter(declaration.initializer, declaration.initializerStartIndex, fileContent, nextVisitedIdentifiers);
7411
+ };
7412
+ const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers, visitedCallSites) => {
7413
+ const trimmedExpression = expression.trim();
7414
+ if (isExplicitlyTrustedHtmlValue(trimmedExpression, fileContent, sinkIndex)) return false;
7415
+ const identifier = trimmedExpression.match(/^([\w$]+)(?:\.|\s*(?:[;,})\n]|$))/)?.[1];
7416
+ if (identifier === void 0) return HTML_TAINT_PATTERN.test(trimmedExpression);
7417
+ if (visitedIdentifiers.has(identifier)) return false;
7176
7418
  visitedIdentifiers.add(identifier);
7177
7419
  const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
7178
- if (declaration !== null && isHtmlTainted(declaration.initializer, fileContent, sinkIndex, visitedIdentifiers)) return true;
7179
7420
  const parameterSource = findContainingFunctionParameterSource(identifier, sinkIndex, fileContent);
7180
- if (parameterSource === null || parameterSource.functionName.length === 0) return false;
7181
- const callPattern = new RegExp(`\\b${escapeRegExp(parameterSource.functionName)}\\s*\\(([^)]*)\\)`, "g");
7182
- for (const callMatch of fileContent.matchAll(callPattern)) {
7183
- const argument = splitTopLevelArguments(callMatch[1] ?? "")[parameterSource.parameterIndex];
7184
- if (argument !== void 0 && isHtmlTainted(argument, fileContent, sinkIndex, new Set(visitedIdentifiers))) return true;
7185
- }
7186
- return false;
7421
+ if (declaration !== null && (parameterSource === null || declaration.declarationIndex > parameterSource.declarationNameIndex)) {
7422
+ if (isDeclarationStable(identifier, declaration, sinkIndex, fileContent) && isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex)) return false;
7423
+ if (!isDeclarationStable(identifier, declaration, sinkIndex, fileContent)) return true;
7424
+ if (isHtmlTainted(declaration.initializer, fileContent, declaration.initializerStartIndex, visitedIdentifiers, visitedCallSites)) return true;
7425
+ if (isBackedByFunctionParameter(declaration.initializer, declaration.initializerStartIndex, fileContent)) return false;
7426
+ return HTML_TAINT_PATTERN.test(trimmedExpression);
7427
+ }
7428
+ if (parameterSource !== null && parameterSource.functionName.length > 0) {
7429
+ const callPattern = new RegExp(`\\b${escapeRegExp(parameterSource.functionName)}\\s*\\(`, "g");
7430
+ let didInspectCallArgument = false;
7431
+ let didInspectOnlyExplicitlyTrustedArguments = true;
7432
+ for (const callMatch of fileContent.matchAll(callPattern)) {
7433
+ if (callMatch.index === parameterSource.declarationNameIndex || visitedCallSites.has(callMatch.index)) continue;
7434
+ const openingParenthesisIndex = callMatch.index + callMatch[0].lastIndexOf("(");
7435
+ const closingParenthesisIndex = findMatchingBracket(fileContent, openingParenthesisIndex);
7436
+ if (closingParenthesisIndex < 0) continue;
7437
+ const argument = splitTopLevelArguments(fileContent.slice(openingParenthesisIndex + 1, closingParenthesisIndex))[parameterSource.parameterIndex];
7438
+ if (argument === void 0) continue;
7439
+ if (callMatch.index >= parameterSource.declarationNameIndex && callMatch.index <= parameterSource.bodyEndIndex && doesExpressionAliasIdentifier(argument, identifier, callMatch.index, fileContent)) continue;
7440
+ didInspectCallArgument = true;
7441
+ didInspectOnlyExplicitlyTrustedArguments &&= isExplicitlyTrustedHtmlValue(argument, fileContent, callMatch.index);
7442
+ const callVisitedIdentifiers = new Set(visitedIdentifiers);
7443
+ callVisitedIdentifiers.delete(identifier);
7444
+ const nextVisitedCallSites = new Set(visitedCallSites);
7445
+ nextVisitedCallSites.add(callMatch.index);
7446
+ if (isHtmlTainted(argument, fileContent, callMatch.index, callVisitedIdentifiers, nextVisitedCallSites)) return true;
7447
+ }
7448
+ if (didInspectCallArgument) return !didInspectOnlyExplicitlyTrustedArguments && HTML_TAINT_PATTERN.test(trimmedExpression);
7449
+ return HTML_TAINT_PATTERN.test(trimmedExpression);
7450
+ }
7451
+ return HTML_TAINT_PATTERN.test(trimmedExpression);
7187
7452
  };
7188
7453
  const dangerousHtmlSink = defineRule({
7189
7454
  id: "dangerous-html-sink",
@@ -7222,7 +7487,7 @@ const dangerousHtmlSink = defineRule({
7222
7487
  const longValueTail = HTML_VALUE_START_PATTERN.exec(lines.slice(lineIndex, lineIndex + 1 + STATIC_TEMPLATE_LOOKAHEAD_LINES).join("\n"))?.[1]?.trimStart();
7223
7488
  let templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
7224
7489
  const valueIdentifier = BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression) || MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN.test(valueExpression) || IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN.test(valueExpression) ? valueExpression.match(/^[\w$]+/)?.[0] : void 0;
7225
- if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)) {
7490
+ if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression) && findContainingFunctionParameterSource(valueIdentifier, sinkIndex, file.content) === null) {
7226
7491
  const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, sinkIndex, file.content);
7227
7492
  if (declarationInitializer !== null) {
7228
7493
  if (isPureStringLiteralConcat(declarationInitializer)) continue;
@@ -7231,17 +7496,23 @@ const dangerousHtmlSink = defineRule({
7231
7496
  }
7232
7497
  if (templateInterpolations === "") continue;
7233
7498
  const judgedExpression = templateInterpolations ?? valueExpression;
7234
- if (SANITIZER_PATTERN.test(judgedExpression)) continue;
7235
- if (ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
7236
- if (I18N_VALUE_PATTERN.test(judgedExpression)) continue;
7237
- if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set())) continue;
7499
+ const doesJudgedExpressionCombineValues = splitTopLevelByPlus(judgedExpression).length > 1 || (templateInterpolations?.match(/\$\{/g)?.length ?? 0) > 1;
7500
+ if (!doesJudgedExpressionCombineValues && SANITIZER_PATTERN.test(judgedExpression)) continue;
7501
+ if (!doesJudgedExpressionCombineValues && ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
7502
+ if (!doesJudgedExpressionCombineValues && I18N_VALUE_PATTERN.test(judgedExpression)) continue;
7503
+ if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set())) continue;
7238
7504
  if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
7239
- if (/highlighted/i.test(valueExpression)) continue;
7240
- if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
7505
+ if (isTrustedHighlighterValue(valueExpression, file.content, sinkIndex)) continue;
7241
7506
  if (valueIdentifier !== void 0) {
7242
7507
  const escapedIdentifier = escapeRegExp(valueIdentifier);
7243
- if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
7244
- if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
7508
+ const visibleDeclaration = findVisibleIdentifierDeclaration(valueIdentifier, sinkIndex, file.content);
7509
+ const visibleInitializer = visibleDeclaration?.initializer;
7510
+ if (!(visibleInitializer !== void 0 && (splitTopLevelByPlus(visibleInitializer).length > 1 || (visibleInitializer.match(/\$\{/g)?.length ?? 0) > 1))) {
7511
+ const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
7512
+ if (visibleInitializer === void 0 ? fromSerializer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(visibleInitializer)) continue;
7513
+ const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
7514
+ if (visibleInitializer === void 0 ? fromSanitizer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SANITIZED_ASSIGNMENT_PATTERN.test(`=${visibleInitializer}`)) continue;
7515
+ }
7245
7516
  if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
7246
7517
  if (new RegExp(`highlight[\\w$]*\\s*\\.map\\(\\s*(?:async\\s+)?\\(?\\s*${escapedIdentifier}\\b`, "i").test(file.content) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
7247
7518
  }
@@ -7326,7 +7597,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
7326
7597
  const getOpeningElementTagName = (openingElement) => {
7327
7598
  if (!openingElement) return null;
7328
7599
  if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
7329
- if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement.name.name;
7600
+ if (isNodeOfType(openingElement.name, "JSXIdentifier")) return resolveJsxElementType(openingElement);
7330
7601
  if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
7331
7602
  let cursor = openingElement.name;
7332
7603
  while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
@@ -7578,7 +7849,7 @@ const dialogHasAccessibleName = defineRule({
7578
7849
  if (isTestlikeFilename(context.filename)) return {};
7579
7850
  return { JSXOpeningElement(node) {
7580
7851
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
7581
- const tagName = node.name.name;
7852
+ const tagName = resolveJsxElementType(node);
7582
7853
  if (tagName[0] !== tagName[0]?.toLowerCase()) return;
7583
7854
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
7584
7855
  const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
@@ -9070,6 +9341,30 @@ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, conte
9070
9341
  }
9071
9342
  return matchingBlocks.size > 0;
9072
9343
  };
9344
+ const doMatchingNodesCoverEveryPathFromFunctionEntry = (owner, matchingNodes, context) => {
9345
+ const functionCfg = context.cfg.cfgFor(owner);
9346
+ if (!functionCfg) return false;
9347
+ const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
9348
+ if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
9349
+ const matchingBlock = functionCfg.blockOf(matchingNode);
9350
+ return matchingBlock ? [matchingBlock] : [];
9351
+ }));
9352
+ if (matchingBlocks.size === 0) return false;
9353
+ const visitedBlocks = new Set([functionCfg.entry]);
9354
+ const pendingBlocks = [functionCfg.entry];
9355
+ while (pendingBlocks.length > 0) {
9356
+ const currentBlock = pendingBlocks.pop();
9357
+ if (!currentBlock) break;
9358
+ if (matchingBlocks.has(currentBlock)) continue;
9359
+ for (const edge of currentBlock.successors) {
9360
+ if (edge.to === functionCfg.exit) return false;
9361
+ if (visitedBlocks.has(edge.to)) continue;
9362
+ visitedBlocks.add(edge.to);
9363
+ pendingBlocks.push(edge.to);
9364
+ }
9365
+ }
9366
+ return true;
9367
+ };
9073
9368
  const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
9074
9369
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
9075
9370
  if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
@@ -9270,6 +9565,83 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
9270
9565
  });
9271
9566
  return didCleanupFunctionMatch;
9272
9567
  };
9568
+ const doesBoundCleanupReleaseUsage = (expression, usage, context) => {
9569
+ const callExpression = stripParenExpression(expression);
9570
+ if (!isNodeOfType(callExpression, "CallExpression")) return false;
9571
+ const bindCallee = stripParenExpression(callExpression.callee);
9572
+ if (!isNodeOfType(bindCallee, "MemberExpression") || bindCallee.computed || !isNodeOfType(bindCallee.property, "Identifier") || bindCallee.property.name !== "bind") return false;
9573
+ const releaseMember = stripParenExpression(bindCallee.object);
9574
+ if (!isNodeOfType(releaseMember, "MemberExpression") || releaseMember.computed || !isNodeOfType(releaseMember.property, "Identifier")) return false;
9575
+ const releaseReceiverKey = resolveExpressionKey$1(releaseMember.object, context);
9576
+ if (releaseReceiverKey === null || releaseReceiverKey !== resolveExpressionKey$1(callExpression.arguments?.[0], context)) return false;
9577
+ const releaseVerbName = releaseMember.property.name;
9578
+ if (usage.kind === "socket") return usage.handleKey === releaseReceiverKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
9579
+ return usage.kind === "subscribe" && usage.handleKey === releaseReceiverKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName));
9580
+ };
9581
+ const callbackReturnsCleanupForUsage = (callback, usage, context) => {
9582
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
9583
+ const doesReturnedValueReleaseUsage = (returnedValue) => {
9584
+ if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) return true;
9585
+ const cleanupFunction = resolveStableValue(returnedValue, context);
9586
+ if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) return true;
9587
+ return Boolean(cleanupFunction && isFunctionLike$1(cleanupFunction) && doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context));
9588
+ };
9589
+ if (!isNodeOfType(callback.body, "BlockStatement")) return doesReturnedValueReleaseUsage(stripParenExpression(callback.body));
9590
+ const matchingCleanupReturns = [];
9591
+ walkInsideStatementBlocks(callback.body, (child) => {
9592
+ if (isNodeOfType(child, "ReturnStatement") && child.argument && doesReturnedValueReleaseUsage(stripParenExpression(child.argument))) matchingCleanupReturns.push(child);
9593
+ });
9594
+ return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
9595
+ };
9596
+ const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
9597
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
9598
+ const functionCfg = context.cfg.cfgFor(callback);
9599
+ const usageBlock = functionCfg?.blockOf(usage.node);
9600
+ const usageStart = getRangeStart(usage.node);
9601
+ if (!functionCfg || !usageBlock || usageStart === null) return false;
9602
+ const findHandleGuard = (releaseCall) => {
9603
+ if (usage.handleKey === null) return null;
9604
+ let ancestor = releaseCall.parent;
9605
+ while (ancestor && ancestor !== callback.body) {
9606
+ if (isNodeOfType(ancestor, "IfStatement")) return ancestor.alternate === null && resolveExpressionKey$1(ancestor.test, context) === usage.handleKey && getRangeStart(ancestor) !== null && (getRangeStart(ancestor) ?? usageStart) < usageStart ? ancestor : null;
9607
+ ancestor = ancestor.parent;
9608
+ }
9609
+ return null;
9610
+ };
9611
+ const matchingReleaseAnchors = [];
9612
+ walkInsideStatementBlocks(callback.body, (child) => {
9613
+ if (!isNodeOfType(child, "CallExpression")) return;
9614
+ const releaseStart = getRangeStart(child);
9615
+ const handleGuard = findHandleGuard(child);
9616
+ if (releaseStart === null || releaseStart >= usageStart || functionCfg.blockOf(child) !== usageBlock && !handleGuard) return;
9617
+ if (doesReleaseCallMatchUsage(child, usage, context)) {
9618
+ matchingReleaseAnchors.push(handleGuard ?? child);
9619
+ return;
9620
+ }
9621
+ const helperFunction = resolveStableValue(child.callee, context);
9622
+ if (helperFunction && isFunctionLike$1(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context)) matchingReleaseAnchors.push(handleGuard ?? child);
9623
+ });
9624
+ return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingReleaseAnchors, context);
9625
+ };
9626
+ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
9627
+ const componentFunction = findEnclosingFunction(callback);
9628
+ if (!componentFunction || !isNodeOfType(componentFunction, "ArrowFunctionExpression") && !isNodeOfType(componentFunction, "FunctionExpression") && !isNodeOfType(componentFunction, "FunctionDeclaration")) return false;
9629
+ let didFindUnmountCleanup = false;
9630
+ walkAst(componentFunction.body, (child) => {
9631
+ if (didFindUnmountCleanup) return false;
9632
+ if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction(child) !== componentFunction) return;
9633
+ if (!isHookCall$2(child, CLEANUP_EFFECT_HOOK_NAMES)) return;
9634
+ const dependencyList = child.arguments?.[1];
9635
+ if (!isNodeOfType(dependencyList, "ArrayExpression") || dependencyList.elements.length > 0) return;
9636
+ const cleanupCallback = getEffectCallback(child);
9637
+ if (cleanupCallback && cleanupCallback !== callback && callbackReturnsCleanupForUsage(cleanupCallback, usage, context)) {
9638
+ didFindUnmountCleanup = true;
9639
+ return false;
9640
+ }
9641
+ });
9642
+ return didFindUnmountCleanup;
9643
+ };
9644
+ const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
9273
9645
  const effectHasCleanupForUsage = (callback, usage, context) => {
9274
9646
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
9275
9647
  if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
@@ -9282,6 +9654,10 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
9282
9654
  if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
9283
9655
  const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
9284
9656
  if (!returnedValue) return;
9657
+ if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) {
9658
+ matchingCleanupReturns.push(child);
9659
+ return;
9660
+ }
9285
9661
  if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
9286
9662
  matchingCleanupReturns.push(child);
9287
9663
  return;
@@ -9297,13 +9673,17 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
9297
9673
  if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
9298
9674
  }
9299
9675
  const cleanupFunction = resolveStableValue(returnedValue, context);
9676
+ if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) {
9677
+ matchingCleanupReturns.push(child);
9678
+ return;
9679
+ }
9300
9680
  if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
9301
9681
  if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
9302
9682
  });
9303
9683
  return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
9304
9684
  };
9305
9685
  const findFirstUsageWithoutCleanup = (callback, usages, context) => {
9306
- for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context)) return usage;
9686
+ for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context) && !hasSplitLifecycleCleanup(callback, usage, context)) return usage;
9307
9687
  return null;
9308
9688
  };
9309
9689
  const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
@@ -11701,7 +12081,7 @@ const forbidDomProps = defineRule({
11701
12081
  return { JSXOpeningElement(node) {
11702
12082
  if (forbidMap.size === 0) return;
11703
12083
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
11704
- const elementName = node.name.name;
12084
+ const elementName = resolveJsxElementType(node);
11705
12085
  if (isReactComponentName(elementName)) return;
11706
12086
  for (const attribute of node.attributes) {
11707
12087
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -11779,7 +12159,7 @@ const forbidElements = defineRule({
11779
12159
  return {
11780
12160
  JSXOpeningElement(node) {
11781
12161
  if (forbidMap.size === 0) return;
11782
- const fullName = flattenJsxName$1(node.name);
12162
+ const fullName = resolveJsxElementType(node);
11783
12163
  if (!fullName || !forbidMap.has(fullName)) return;
11784
12164
  context.report({
11785
12165
  node: node.name,
@@ -12141,8 +12521,7 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
12141
12521
  const isParagraphElement = (candidate) => {
12142
12522
  if (!isNodeOfType(candidate, "JSXElement")) return false;
12143
12523
  const opening = candidate.openingElement;
12144
- if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
12145
- return opening.name.name === "p";
12524
+ return resolveJsxElementType(opening) === "p";
12146
12525
  };
12147
12526
  const findEnclosingParagraph = (openingElement) => {
12148
12527
  const owningElement = openingElement.parent;
@@ -12163,8 +12542,7 @@ const htmlNoInvalidParagraphChild = defineRule({
12163
12542
  severity: "warn",
12164
12543
  recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
12165
12544
  create: (context) => ({ JSXOpeningElement(node) {
12166
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
12167
- const childTagName = node.name.name;
12545
+ const childTagName = resolveJsxElementType(node);
12168
12546
  if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
12169
12547
  if (!findEnclosingParagraph(node)) return;
12170
12548
  context.report({
@@ -12195,7 +12573,7 @@ const getHostTagName = (jsxElement) => {
12195
12573
  if (!isNodeOfType(jsxElement, "JSXElement")) return null;
12196
12574
  const opening = jsxElement.openingElement;
12197
12575
  if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
12198
- const tagName = opening.name.name;
12576
+ const tagName = resolveJsxElementType(opening);
12199
12577
  if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
12200
12578
  return tagName;
12201
12579
  };
@@ -12225,7 +12603,7 @@ const findClosestHostAncestor = (jsxElement) => {
12225
12603
  if (isNodeOfType(ancestor, "JSXElement")) {
12226
12604
  const opening = ancestor.openingElement;
12227
12605
  if (isNodeOfType(opening.name, "JSXIdentifier")) {
12228
- const ancestorTag = opening.name.name;
12606
+ const ancestorTag = resolveJsxElementType(opening);
12229
12607
  if (ancestorTag.length === 0) {
12230
12608
  previous = ancestor;
12231
12609
  ancestor = ancestor.parent ?? null;
@@ -12307,8 +12685,7 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
12307
12685
  const isJsxElementWithTagName = (candidate, tagName) => {
12308
12686
  if (!isNodeOfType(candidate, "JSXElement")) return false;
12309
12687
  const opening = candidate.openingElement;
12310
- if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
12311
- return opening.name.name === tagName;
12688
+ return resolveJsxElementType(opening) === tagName;
12312
12689
  };
12313
12690
  const findEnclosingSameTag = (openingElement, tagName) => {
12314
12691
  const owningElement = openingElement.parent;
@@ -12329,8 +12706,7 @@ const htmlNoNestedInteractive = defineRule({
12329
12706
  severity: "warn",
12330
12707
  recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
12331
12708
  create: (context) => ({ JSXOpeningElement(node) {
12332
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
12333
- const tagName = node.name.name;
12709
+ const tagName = resolveJsxElementType(node);
12334
12710
  if (tagName !== "a" && tagName !== "button") return;
12335
12711
  if (!findEnclosingSameTag(node, tagName)) return;
12336
12712
  context.report({
@@ -12445,7 +12821,7 @@ const iframeMissingSandbox = defineRule({
12445
12821
  matchByOccurrence: true,
12446
12822
  create: skipNonProductionFiles((context) => ({
12447
12823
  JSXOpeningElement(node) {
12448
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "iframe") return;
12824
+ if (resolveJsxElementType(node) !== "iframe") return;
12449
12825
  const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
12450
12826
  if (!sandboxAttr) {
12451
12827
  const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
@@ -12681,30 +13057,6 @@ const insecureCryptoRisk = defineRule({
12681
13057
  }
12682
13058
  });
12683
13059
  //#endregion
12684
- //#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
12685
- const findMatchingBracket = (content, openIndex) => {
12686
- const open = content[openIndex];
12687
- const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
12688
- if (close === "") return -1;
12689
- let depth = 0;
12690
- let stringDelimiter = null;
12691
- for (let index = openIndex; index < content.length; index += 1) {
12692
- const character = content[index];
12693
- if (stringDelimiter !== null) {
12694
- if (character === "\\") index += 1;
12695
- else if (character === stringDelimiter) stringDelimiter = null;
12696
- continue;
12697
- }
12698
- if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
12699
- else if (character === open) depth += 1;
12700
- else if (character === close) {
12701
- depth -= 1;
12702
- if (depth === 0) return index;
12703
- }
12704
- }
12705
- return -1;
12706
- };
12707
- //#endregion
12708
13060
  //#region src/plugin/rules/security-scan/insecure-session-cookie.ts
12709
13061
  const AUTH_COOKIE_NAME_TOKEN = `(?<![A-Za-z0-9])(?:session|sess|sid|connect\\.sid|auth|jwt|access[_-]?token|refresh[_-]?token|id[_-]?token)(?![A-Za-z0-9])`;
12710
13062
  const AUTH_COOKIE_NAME_LITERAL = `[\`"'][^\`"']*?${AUTH_COOKIE_NAME_TOKEN}[^\`"']*[\`"']`;
@@ -13461,6 +13813,28 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
13461
13813
  } })
13462
13814
  });
13463
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
13464
13838
  //#region src/plugin/rules/js-performance/js-batch-dom-css.ts
13465
13839
  const ITERATOR_METHOD_NAMES$2 = new Set([
13466
13840
  "forEach",
@@ -13645,9 +14019,19 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
13645
14019
  });
13646
14020
  return foundAttachment;
13647
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
+ };
13648
14031
  const isProvablyDetachedAtWrite = (styleWriteStatement) => {
13649
- if (!isNodeOfType(styleWriteStatement, "ExpressionStatement") || !isNodeOfType(styleWriteStatement.expression, "AssignmentExpression") || !isNodeOfType(styleWriteStatement.expression.left, "MemberExpression") || !isNodeOfType(styleWriteStatement.expression.left.object, "MemberExpression")) return false;
13650
- 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;
13651
14035
  const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
13652
14036
  if (!creationRoot) return false;
13653
14037
  return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
@@ -13659,15 +14043,17 @@ const jsBatchDomCss = defineRule({
13659
14043
  severity: "warn",
13660
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",
13661
14045
  create: (context) => {
13662
- 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";
13663
- 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
+ };
13664
14050
  return { BlockStatement(node) {
13665
14051
  const perIterationBody = findEnclosingPerIterationBody(node);
13666
14052
  if (!perIterationBody) return;
13667
14053
  const statements = node.body ?? [];
13668
14054
  let layoutReads = null;
13669
14055
  for (let statementIndex = 1; statementIndex < statements.length; statementIndex++) {
13670
- if (!isStyleAssignment(statements[statementIndex]) || !isStyleAssignment(statements[statementIndex - 1])) continue;
14056
+ if (getStyleAssignment(statements[statementIndex]) === null || getStyleAssignment(statements[statementIndex - 1]) === null) continue;
13671
14057
  if (!writesLayoutAffectingProperty(statements[statementIndex]) && !writesLayoutAffectingProperty(statements[statementIndex - 1])) continue;
13672
14058
  layoutReads ??= scanPerIterationLayoutReads(perIterationBody);
13673
14059
  if (!layoutReads.hasUsedLayoutRead || layoutReads.hasDeliberateForcedReflow) return;
@@ -14363,6 +14749,18 @@ const jsHoistRegexp = defineRule({
14363
14749
  }, { treatIteratorCallbacksAsLoops: true })
14364
14750
  });
14365
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
14366
14764
  //#region src/plugin/rules/js-performance/js-index-maps.ts
14367
14765
  const referencesParameter = (expression, parameterName) => {
14368
14766
  if (!expression) return false;
@@ -14373,7 +14771,7 @@ const referencesParameter = (expression, parameterName) => {
14373
14771
  const isSingleFieldEqualityPredicate = (node) => {
14374
14772
  const callback = node.arguments?.[0];
14375
14773
  if (!isInlineFunctionExpression(callback)) return false;
14376
- const firstParameter = callback.params?.[0];
14774
+ const firstParameter = resolveFirstArgumentBinding(callback.params?.[0]);
14377
14775
  if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
14378
14776
  let predicate = null;
14379
14777
  const body = callback.body;
@@ -15065,9 +15463,21 @@ const isSmallFixedListMember = (receiver) => {
15065
15463
  };
15066
15464
  const getResolvedInitializer = (receiver) => {
15067
15465
  if (!isNodeOfType(receiver, "Identifier")) return null;
15068
- const initializer = findVariableInitializer(receiver, receiver.name)?.initializer ?? null;
15069
- if (initializer && isNodeOfType(initializer, "Identifier")) return findVariableInitializer(initializer, initializer.name)?.initializer ?? initializer;
15070
- 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
+ };
15071
15481
  };
15072
15482
  const isSplitCall = (expression) => {
15073
15483
  if (!expression) return false;
@@ -15233,8 +15643,8 @@ const isBoundedConstantCollection = (collection) => {
15233
15643
  if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
15234
15644
  if (isSmallInlineLiteralArray(stripped)) return true;
15235
15645
  if (isNodeOfType(stripped, "Identifier")) {
15236
- const initializer = getResolvedInitializer(stripped);
15237
- if (initializer && isSmallInlineLiteralArray(initializer)) return true;
15646
+ const resolved = getResolvedInitializer(stripped);
15647
+ if (resolved && !resolved.isDefault && isSmallInlineLiteralArray(resolved.initializer)) return true;
15238
15648
  }
15239
15649
  return false;
15240
15650
  };
@@ -15286,8 +15696,8 @@ const jsSetMapLookups = defineRule({
15286
15696
  if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
15287
15697
  const resolvedInitializer = getResolvedInitializer(receiver);
15288
15698
  if (resolvedInitializer) {
15289
- if (isLikelyStringReceiver(resolvedInitializer)) return;
15290
- if (isSmallInlineLiteralArray(resolvedInitializer)) return;
15699
+ if (isLikelyStringReceiver(resolvedInitializer.initializer)) return;
15700
+ if (!resolvedInitializer.isDefault && isSmallInlineLiteralArray(resolvedInitializer.initializer)) return;
15291
15701
  }
15292
15702
  if (isStringElementOfSplitIteration(receiver)) return;
15293
15703
  if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
@@ -15700,14 +16110,21 @@ const jsxFilenameExtension = defineRule({
15700
16110
  });
15701
16111
  //#endregion
15702
16112
  //#region src/plugin/utils/is-jsx-fragment-element.ts
15703
- const isJsxFragmentElement = (node) => {
16113
+ const isJsxFragmentElement = (node, scopes) => {
15704
16114
  if (!isNodeOfType(node, "JSXOpeningElement")) return false;
15705
16115
  const elementName = node.name;
15706
- 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
+ }
15707
16122
  if (isNodeOfType(elementName, "JSXMemberExpression")) {
15708
16123
  if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
15709
- if (elementName.object.name !== "React") return false;
15710
- 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);
15711
16128
  }
15712
16129
  return false;
15713
16130
  };
@@ -15733,7 +16150,7 @@ const jsxFragments = defineRule({
15733
16150
  if (mode !== "syntax") return;
15734
16151
  if (!node.closingElement) return;
15735
16152
  const openingElement = node.openingElement;
15736
- if (!isJsxFragmentElement(openingElement)) return;
16153
+ if (!isJsxFragmentElement(openingElement, context.scopes)) return;
15737
16154
  if (openingElement.attributes.length > 0) return;
15738
16155
  context.report({
15739
16156
  node: openingElement,
@@ -18414,10 +18831,6 @@ const resolveSettings$29 = (settings) => {
18414
18831
  if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
18415
18832
  return reactDoctor.jsxNoScriptUrl ?? {};
18416
18833
  };
18417
- const getElementName = (node) => {
18418
- if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
18419
- return null;
18420
- };
18421
18834
  const isLinkPropForElement = (elementName, attributeName, options) => {
18422
18835
  if (elementName === "a" && attributeName === "href") return true;
18423
18836
  const explicit = options.components?.[elementName];
@@ -18437,7 +18850,8 @@ const jsxNoScriptUrl = defineRule({
18437
18850
  create: (context) => {
18438
18851
  const options = resolveSettings$29(context.settings);
18439
18852
  return { JSXOpeningElement(node) {
18440
- const elementName = getElementName(node);
18853
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
18854
+ const elementName = resolveJsxElementType(node);
18441
18855
  if (!elementName) return;
18442
18856
  for (const attribute of node.attributes) {
18443
18857
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -18624,11 +19038,6 @@ const checkTarget = (attributeValue) => {
18624
19038
  alternate: false
18625
19039
  };
18626
19040
  };
18627
- const getOpeningElementName = (node) => {
18628
- const name = node.name;
18629
- if (isNodeOfType(name, "JSXIdentifier")) return name.name;
18630
- return null;
18631
- };
18632
19041
  const jsxNoTargetBlank = defineRule({
18633
19042
  id: "jsx-no-target-blank",
18634
19043
  title: "Unsafe target=_blank link",
@@ -18648,7 +19057,8 @@ const jsxNoTargetBlank = defineRule({
18648
19057
  return settings.formComponents.has(tagName);
18649
19058
  };
18650
19059
  return { JSXOpeningElement(node) {
18651
- const tagName = getOpeningElementName(node);
19060
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
19061
+ const tagName = resolveJsxElementType(node);
18652
19062
  if (!tagName) return;
18653
19063
  if (!isLink(tagName) && !isForm(tagName)) return;
18654
19064
  const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
@@ -18881,7 +19291,7 @@ const jsxNoUselessFragment = defineRule({
18881
19291
  return {
18882
19292
  JSXElement(node) {
18883
19293
  const openingElement = node.openingElement;
18884
- if (!isJsxFragmentElement(openingElement)) return;
19294
+ if (!isJsxFragmentElement(openingElement, context.scopes)) return;
18885
19295
  if (hasJsxKeyAttribute(openingElement)) return;
18886
19296
  if (checkChildren(node, openingElement, node.children)) return;
18887
19297
  if (isChildOfHtmlElement(node)) context.report({
@@ -20123,11 +20533,24 @@ const hasDirective = (programNode, directive) => {
20123
20533
  return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
20124
20534
  };
20125
20535
  //#endregion
20126
- //#region src/plugin/utils/is-uppercase-name.ts
20127
- const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
20128
- //#endregion
20129
- //#region src/plugin/utils/is-component-assignment.ts
20130
- 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
+ };
20131
20554
  //#endregion
20132
20555
  //#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
20133
20556
  const nextjsAsyncClientComponent = defineRule({
@@ -20153,10 +20576,10 @@ const nextjsAsyncClientComponent = defineRule({
20153
20576
  },
20154
20577
  VariableDeclarator(node) {
20155
20578
  if (!fileHasUseClient) return;
20156
- if (!isComponentAssignment(node)) return;
20157
- if (!isInlineFunctionExpression(node.init)) return;
20158
- if (!node.init.async) return;
20159
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;
20160
20583
  context.report({
20161
20584
  node,
20162
20585
  message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
@@ -20458,7 +20881,7 @@ const nextjsNoAElement = defineRule({
20458
20881
  severity: "warn",
20459
20882
  recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
20460
20883
  create: (context) => ({ JSXOpeningElement(node) {
20461
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
20884
+ if (resolveJsxElementType(node) !== "a") return;
20462
20885
  const attributes = node.attributes ?? [];
20463
20886
  const downloadAttribute = findJsxAttribute(attributes, "download");
20464
20887
  if (downloadAttribute) {
@@ -20654,7 +21077,7 @@ const nextjsNoCssLink = defineRule({
20654
21077
  severity: "warn",
20655
21078
  recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
20656
21079
  create: (context) => ({ JSXOpeningElement(node) {
20657
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
21080
+ if (resolveJsxElementType(node) !== "link") return;
20658
21081
  const attributes = node.attributes ?? [];
20659
21082
  const relAttribute = findJsxAttribute(attributes, "rel");
20660
21083
  if (!relAttribute?.value) return;
@@ -20762,7 +21185,7 @@ const nextjsNoFontLink = defineRule({
20762
21185
  severity: "warn",
20763
21186
  recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
20764
21187
  create: (context) => ({ JSXOpeningElement(node) {
20765
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
21188
+ if (resolveJsxElementType(node) !== "link") return;
20766
21189
  const attributes = node.attributes ?? [];
20767
21190
  const hrefAttribute = findJsxAttribute(attributes, "href");
20768
21191
  if (!hrefAttribute?.value) return;
@@ -20937,7 +21360,7 @@ const nextjsNoImgElement = defineRule({
20937
21360
  create: (context) => {
20938
21361
  if (isGeneratedImageRenderContext(context)) return {};
20939
21362
  return { JSXOpeningElement(node) {
20940
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
21363
+ if (resolveJsxElementType(node) !== "img") return;
20941
21364
  if (isGeneratedImageRenderContext(context, node)) return;
20942
21365
  const programRoot = findProgramRoot(node);
20943
21366
  if (programRoot && hasEmailTemplateImport(programRoot)) return;
@@ -20984,8 +21407,8 @@ const nextjsNoPolyfillScript = defineRule({
20984
21407
  severity: "warn",
20985
21408
  recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
20986
21409
  create: (context) => ({ JSXOpeningElement(node) {
20987
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
20988
- if (node.name.name !== "script" && node.name.name !== "Script") return;
21410
+ const elementName = resolveJsxElementType(node);
21411
+ if (elementName !== "script" && elementName !== "Script") return;
20989
21412
  const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
20990
21413
  if (!srcAttribute?.value) return;
20991
21414
  const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
@@ -22793,10 +23216,15 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
22793
23216
  if (!declaringNode) return false;
22794
23217
  return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
22795
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
+ };
22796
23225
  const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
22797
23226
  if (def.type !== "Parameter") return false;
22798
- const bindingParent = def.name.parent;
22799
- return isFunctionLike$1(bindingParent);
23227
+ return isWholePropsParameterBinding(def.name);
22800
23228
  }));
22801
23229
  const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
22802
23230
  const isPropAlias = (analysis, ref) => {
@@ -25793,6 +26221,56 @@ const noBarrelImport = defineRule({
25793
26221
  }
25794
26222
  });
25795
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
25796
26274
  //#region src/plugin/utils/function-contains-react-render-output.ts
25797
26275
  const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
25798
26276
  "FunctionDeclaration",
@@ -25812,12 +26290,13 @@ const isCallArgumentFunctionExpression = (node) => {
25812
26290
  return parent.arguments.some((argumentNode) => argumentNode === node);
25813
26291
  };
25814
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);
25815
26294
  const containsRenderOutput$1 = (rootNode, scopes) => {
25816
26295
  let hasRenderOutput = false;
25817
26296
  walkAst(rootNode, (node) => {
25818
26297
  if (hasRenderOutput) return false;
25819
26298
  if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
25820
- if (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
26299
+ if (isRenderOutputExpression(node, scopes)) {
25821
26300
  hasRenderOutput = true;
25822
26301
  return false;
25823
26302
  }
@@ -25828,7 +26307,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
25828
26307
  const functionContainsReactRenderOutput = (functionNode, scopes) => {
25829
26308
  const cachedEntry = renderOutputCache.get(functionNode);
25830
26309
  if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
25831
- const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
26310
+ const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
25832
26311
  renderOutputCache.set(functionNode, {
25833
26312
  scopes,
25834
26313
  hasRenderOutput
@@ -25836,6 +26315,9 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
25836
26315
  return hasRenderOutput;
25837
26316
  };
25838
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
25839
26321
  //#region src/plugin/utils/is-component-declaration.ts
25840
26322
  const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
25841
26323
  //#endregion
@@ -28293,7 +28775,7 @@ const noDisabledZoom = defineRule({
28293
28775
  category: "Accessibility",
28294
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.",
28295
28777
  create: (context) => ({ JSXOpeningElement(node) {
28296
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
28778
+ if (resolveJsxElementType(node) !== "meta") return;
28297
28779
  const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
28298
28780
  if (!nameAttr?.value) return;
28299
28781
  if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
@@ -28683,7 +29165,7 @@ const findTopLevelEffectCalls = (componentBody) => {
28683
29165
  if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
28684
29166
  for (const statement of componentBody.body ?? []) {
28685
29167
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
28686
- const expression = statement.expression;
29168
+ const expression = unwrapDiscardedExpression(statement);
28687
29169
  if (!isNodeOfType(expression, "CallExpression")) continue;
28688
29170
  if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
28689
29171
  effectCalls.push(expression);
@@ -31465,7 +31947,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
31465
31947
  severity: "warn",
31466
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.",
31467
31949
  create: (context) => ({ JSXOpeningElement(node) {
31468
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
31950
+ if (resolveJsxElementType(node) !== "img") return;
31469
31951
  const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
31470
31952
  if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
31471
31953
  const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
@@ -31706,7 +32188,7 @@ const noIndeterminateAttribute = defineRule({
31706
32188
  if (classifyReactNativeFileTarget(context) === "react-native") return {};
31707
32189
  return {
31708
32190
  JSXOpeningElement(node) {
31709
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
32191
+ if (resolveJsxElementType(node) !== "input") return;
31710
32192
  let typeAttribute = null;
31711
32193
  let typeAttributeIndex = null;
31712
32194
  let indeterminateAttribute = null;
@@ -31885,11 +32367,12 @@ const isMemoCall = (node) => {
31885
32367
  };
31886
32368
  const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
31887
32369
  const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
31888
- const isInlineReference = (node) => {
31889
- 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";
31890
- if (isNodeOfType(node, "ObjectExpression")) return "objects";
31891
- if (isNodeOfType(node, "ArrayExpression")) return "Arrays";
31892
- 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";
31893
32376
  return null;
31894
32377
  };
31895
32378
  const noInlinePropOnMemoComponent = defineRule({
@@ -31919,7 +32402,7 @@ const noInlinePropOnMemoComponent = defineRule({
31919
32402
  let elementName = null;
31920
32403
  if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
31921
32404
  if (!elementName || !memoizedComponentNames.has(elementName)) return;
31922
- const propType = isInlineReference(node.value.expression);
32405
+ const propType = isInlineReference(node.value.expression, context.scopes);
31923
32406
  if (propType) context.report({
31924
32407
  node: node.value.expression,
31925
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`
@@ -32778,11 +33261,13 @@ const noManyBooleanProps = defineRule({
32778
33261
  };
32779
33262
  const checkComponent = (functionNode, param, body, componentName, reportNode) => {
32780
33263
  if (!param) return;
33264
+ const propsBinding = resolveFirstArgumentBinding(param);
33265
+ if (!propsBinding) return;
32781
33266
  if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
32782
- if (isNodeOfType(param, "ObjectPattern")) {
33267
+ if (isNodeOfType(propsBinding, "ObjectPattern")) {
32783
33268
  const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
32784
33269
  const booleanLikePropNames = [];
32785
- for (const property of param.properties ?? []) {
33270
+ for (const property of propsBinding.properties ?? []) {
32786
33271
  if (!isNodeOfType(property, "Property")) continue;
32787
33272
  const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
32788
33273
  if (!keyName) continue;
@@ -32793,7 +33278,7 @@ const noManyBooleanProps = defineRule({
32793
33278
  reportIfMany(booleanLikePropNames, componentName, reportNode);
32794
33279
  return;
32795
33280
  }
32796
- if (isNodeOfType(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
33281
+ if (isNodeOfType(propsBinding, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, propsBinding.name)], componentName, reportNode);
32797
33282
  };
32798
33283
  return {
32799
33284
  FunctionDeclaration(node) {
@@ -32915,7 +33400,7 @@ const noMirrorPropEffect = defineRule({
32915
33400
  if (mirrorBindings.length === 0) return;
32916
33401
  for (const statement of componentBody.body ?? []) {
32917
33402
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
32918
- const effectCall = statement.expression;
33403
+ const effectCall = unwrapDiscardedExpression(statement);
32919
33404
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
32920
33405
  if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
32921
33406
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
@@ -32929,7 +33414,7 @@ const noMirrorPropEffect = defineRule({
32929
33414
  const bodyStatements = getCallbackStatements(callback);
32930
33415
  if (bodyStatements.length !== 1) continue;
32931
33416
  const onlyStatement = bodyStatements[0];
32932
- const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
33417
+ const expression = unwrapDiscardedExpression(onlyStatement);
32933
33418
  if (!isNodeOfType(expression, "CallExpression")) continue;
32934
33419
  if (!isNodeOfType(expression.callee, "Identifier")) continue;
32935
33420
  if (!isSetterIdentifier(expression.callee.name)) continue;
@@ -34953,6 +35438,14 @@ const isHandlerBagArgument = (analysis, argument) => {
34953
35438
  };
34954
35439
  const getFunctionalUpdaterDataRefs = (analysis, updater) => getDownstreamRefs(analysis, updater).filter((updaterRef) => !updaterRef.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updater));
34955
35440
  const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
35441
+ const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
35442
+ "useIntersectionObserver",
35443
+ "useMatchMedia",
35444
+ "useMediaQuery",
35445
+ "useResizeObserver",
35446
+ "useVisibility",
35447
+ "useWindowSize"
35448
+ ]);
34956
35449
  const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
34957
35450
  const node = def.node;
34958
35451
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
@@ -34975,6 +35468,19 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
34975
35468
  if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
34976
35469
  return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isProp(analysis, downstreamRef)));
34977
35470
  };
35471
+ const isExternalSubscriptionHookRef = (ref) => {
35472
+ const identifier = ref.identifier;
35473
+ if (!isNodeOfType(identifier, "Identifier")) return false;
35474
+ if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(identifier.name) && isCalleePosition(identifier)) return true;
35475
+ return Boolean(ref.resolved?.defs.some((def) => {
35476
+ const node = def.node;
35477
+ if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
35478
+ const initializer = stripParenExpression(node.init);
35479
+ if (!isNodeOfType(initializer, "CallExpression")) return false;
35480
+ const callee = stripParenExpression(initializer.callee);
35481
+ return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(callee.name);
35482
+ }));
35483
+ };
34978
35484
  const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
34979
35485
  const isCalleePosition = (identifier) => {
34980
35486
  const parent = identifier.parent;
@@ -35036,10 +35542,11 @@ const noPassDataToParent = defineRule({
35036
35542
  if (argumentRef && resolveToFunction(argumentRef)) return [];
35037
35543
  }
35038
35544
  return getDownstreamRefs(analysis, argument);
35039
- }).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
35545
+ }).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
35040
35546
  if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
35041
35547
  if (!argsUpstreamRefs.some((argRef) => {
35042
35548
  if (isUseStateIdentifier(argRef.identifier)) return false;
35549
+ if (isExternalSubscriptionHookRef(argRef)) return false;
35043
35550
  if (isProp(analysis, argRef)) return false;
35044
35551
  if (isUseRefIdentifier(argRef.identifier)) return false;
35045
35552
  if (isRefCurrent(argRef)) return false;
@@ -35523,7 +36030,7 @@ const noPreventDefault = defineRule({
35523
36030
  const isServerActionsFramework = hasCapability(context.settings, "server-actions");
35524
36031
  const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
35525
36032
  return { JSXOpeningElement(node) {
35526
- const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
36033
+ const elementName = resolveJsxElementType(node);
35527
36034
  if (!elementName) return;
35528
36035
  const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
35529
36036
  if (!targetEventProps) return;
@@ -37008,7 +37515,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
37008
37515
  return expressionReadsStateValue(argument, stateName);
37009
37516
  };
37010
37517
  const getUnconditionalSetterCall = (statement, setterNames) => {
37011
- const expression = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
37518
+ const expression = unwrapDiscardedExpression(statement);
37012
37519
  if (!isNodeOfType(expression, "CallExpression")) return null;
37013
37520
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
37014
37521
  if (!setterNames.has(expression.callee.name)) return null;
@@ -37357,7 +37864,7 @@ const noSelfUpdatingEffect = defineRule({
37357
37864
  const setterNames = new Set(setterNameToStateName.keys());
37358
37865
  for (const statement of functionBody.body ?? []) {
37359
37866
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
37360
- const effectCall = statement.expression;
37867
+ const effectCall = unwrapDiscardedExpression(statement);
37361
37868
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
37362
37869
  if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
37363
37870
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
@@ -37987,9 +38494,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
37987
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.",
37988
38495
  create: (context) => ({ JSXOpeningElement(node) {
37989
38496
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
37990
- const firstCharacter = node.name.name.charCodeAt(0);
38497
+ const elementName = resolveJsxElementType(node);
38498
+ const firstCharacter = elementName.charCodeAt(0);
37991
38499
  if (firstCharacter < 97 || firstCharacter > 122) return;
37992
- if (node.name.name.includes("-")) return;
38500
+ if (elementName.includes("-")) return;
37993
38501
  for (const attribute of node.attributes) {
37994
38502
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
37995
38503
  if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
@@ -38377,8 +38885,7 @@ const noUncontrolledInput = defineRule({
38377
38885
  const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
38378
38886
  walkAst(componentBody, (child) => {
38379
38887
  if (!isNodeOfType(child, "JSXOpeningElement")) return;
38380
- if (!isNodeOfType(child.name, "JSXIdentifier")) return;
38381
- const tagName = child.name.name;
38888
+ const tagName = resolveJsxElementType(child);
38382
38889
  if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
38383
38890
  const attributes = child.attributes ?? [];
38384
38891
  if (hasJsxSpreadAttribute(attributes)) return;
@@ -38439,7 +38946,7 @@ const noUndeferredThirdParty = defineRule({
38439
38946
  severity: "warn",
38440
38947
  recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
38441
38948
  create: (context) => ({ JSXOpeningElement(node) {
38442
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
38949
+ if (resolveJsxElementType(node) !== "script") return;
38443
38950
  const attributes = node.attributes ?? [];
38444
38951
  const srcAttribute = findJsxAttribute(attributes, "src");
38445
38952
  if (!srcAttribute) return;
@@ -39654,7 +40161,7 @@ const noUnknownProperty = defineRule({
39654
40161
  }
39655
40162
  if (fileIsNonReactJsx) return;
39656
40163
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
39657
- const elementType = node.name.name;
40164
+ const elementType = resolveJsxElementType(node);
39658
40165
  const firstCharacter = elementType.charCodeAt(0);
39659
40166
  if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
39660
40167
  let isValidHtmlTag = isKnownDomTag(elementType);
@@ -39832,20 +40339,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
39832
40339
  });
39833
40340
  return found;
39834
40341
  };
40342
+ const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
39835
40343
  const isReactClassComponent = (classNode) => {
39836
40344
  if (isEs6Component(classNode)) return true;
39837
40345
  return expressionContainsJsxOrCreateElement(classNode);
39838
40346
  };
39839
- const findEnclosingComponent = (node) => {
40347
+ const findEnclosingComponent = (node, scopes) => {
39840
40348
  let walker = node.parent;
39841
40349
  while (walker) {
39842
40350
  if (isFunctionLike$1(walker)) {
39843
40351
  const componentName = inferFunctionLikeName(walker);
39844
- if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
40352
+ if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
39845
40353
  component: walker,
39846
40354
  name: componentName
39847
40355
  };
39848
- if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
40356
+ if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
39849
40357
  component: walker,
39850
40358
  name: null
39851
40359
  };
@@ -40084,7 +40592,7 @@ const noUnstableNestedComponents = defineRule({
40084
40592
  if (renderPropRegex.test(propInfo.propName)) return;
40085
40593
  if (settings.allowAsProps) return;
40086
40594
  }
40087
- const enclosing = findEnclosingComponent(candidateNode);
40595
+ const enclosing = findEnclosingComponent(candidateNode, context.scopes);
40088
40596
  if (!enclosing) return;
40089
40597
  const gatedName = propInfo ? null : requiredInstantiationName;
40090
40598
  queuedReports.push({
@@ -40095,7 +40603,7 @@ const noUnstableNestedComponents = defineRule({
40095
40603
  });
40096
40604
  };
40097
40605
  const checkFunctionLike = (node) => {
40098
- if (!expressionContainsJsxOrCreateElement(node)) return;
40606
+ if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
40099
40607
  const inferredName = inferFunctionLikeName(node);
40100
40608
  const propInfo = isComponentDeclaredInProp(node);
40101
40609
  const isObjectCallback = isObjectCallbackCandidate(node);
@@ -41355,7 +41863,7 @@ const preactPreferOndblclick = defineRule({
41355
41863
  recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
41356
41864
  create: (context) => ({ JSXOpeningElement(node) {
41357
41865
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
41358
- const tagName = node.name.name;
41866
+ const tagName = resolveJsxElementType(node);
41359
41867
  if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
41360
41868
  const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
41361
41869
  if (!onDoubleClickAttribute) return;
@@ -41375,7 +41883,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
41375
41883
  ]);
41376
41884
  const isTextLikeInput = (openingElement) => {
41377
41885
  if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
41378
- const tagName = openingElement.name.name;
41886
+ const tagName = resolveJsxElementType(openingElement);
41379
41887
  if (tagName === "textarea") return true;
41380
41888
  if (tagName !== "input") return false;
41381
41889
  const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
@@ -41532,8 +42040,9 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
41532
42040
  ]);
41533
42041
  const collectBooleanPropBindings = (param) => {
41534
42042
  const bindings = /* @__PURE__ */ new Set();
41535
- if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
41536
- 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 ?? []) {
41537
42046
  if (!isNodeOfType(property, "Property")) continue;
41538
42047
  if (property.computed) continue;
41539
42048
  if (!isNodeOfType(property.key, "Identifier")) continue;
@@ -41794,7 +42303,7 @@ const preferHtmlDialog = defineRule({
41794
42303
  },
41795
42304
  JSXOpeningElement(node) {
41796
42305
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
41797
- const tagName = node.name.name;
42306
+ const tagName = resolveJsxElementType(node);
41798
42307
  if (tagName === "dialog") return;
41799
42308
  if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
41800
42309
  if (!HTML_TAGS.has(tagName)) return;
@@ -43341,13 +43850,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
43341
43850
  }
43342
43851
  return null;
43343
43852
  };
43344
- const resolveHookNameFromInitializer = (initializer) => {
43853
+ const resolveHookNameFromInitializer = (initializer, scopes) => {
43345
43854
  if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
43346
43855
  if (!isNodeOfType(initializer, "Identifier")) return null;
43347
- const binding = findVariableInitializer(initializer, initializer.name);
43348
- if (!binding?.initializer || !isConstDeclaredBinding(binding)) return null;
43349
- if (!isNodeOfType(binding.initializer, "CallExpression")) return null;
43350
- 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);
43351
43860
  };
43352
43861
  const queryNoRestDestructuring = defineRule({
43353
43862
  id: "query-no-rest-destructuring",
@@ -43360,7 +43869,7 @@ const queryNoRestDestructuring = defineRule({
43360
43869
  if (!isNodeOfType(node.id, "ObjectPattern")) return;
43361
43870
  if (!node.init) return;
43362
43871
  if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
43363
- const hookName = resolveHookNameFromInitializer(node.init);
43872
+ const hookName = resolveHookNameFromInitializer(node.init, context.scopes);
43364
43873
  if (!hookName) return;
43365
43874
  context.report({
43366
43875
  node: node.id,
@@ -43831,7 +44340,7 @@ const renderingAnimateSvgWrapper = defineRule({
43831
44340
  severity: "warn",
43832
44341
  recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
43833
44342
  create: (context) => ({ JSXOpeningElement(node) {
43834
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
44343
+ if (resolveJsxElementType(node) !== "svg") return;
43835
44344
  if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
43836
44345
  node,
43837
44346
  message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
@@ -45124,14 +45633,10 @@ const rerenderLazyStateInit = defineRule({
45124
45633
  //#endregion
45125
45634
  //#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
45126
45635
  const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
45127
- const callbackReturnsJsx = (callback) => {
45636
+ const callbackReturnsJsx = (callback, scopes) => {
45128
45637
  if (!callback) return false;
45129
45638
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
45130
- const body = callback.body;
45131
- if (isJsxExpression(body)) return true;
45132
- if (!isNodeOfType(body, "BlockStatement")) return false;
45133
- for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
45134
- return false;
45639
+ return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
45135
45640
  };
45136
45641
  const returnArgumentUsesAnyName = (returnStatement, names) => {
45137
45642
  if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
@@ -45209,7 +45714,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
45209
45714
  if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
45210
45715
  for (const declarator of stmt.declarations ?? []) {
45211
45716
  const init = declarator.init;
45212
- if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
45717
+ if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes)) {
45213
45718
  memoNode = declarator;
45214
45719
  callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
45215
45720
  if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
@@ -45275,8 +45780,11 @@ const collectFromObjectPattern = (pattern, bindings) => {
45275
45780
  const collectDefaultedEmptyBindings = (functionNode) => {
45276
45781
  const bindings = /* @__PURE__ */ new Map();
45277
45782
  const params = functionNode.params ?? [];
45278
- for (const param of params) collectFromObjectPattern(param, bindings);
45279
- const propsParam = params[0];
45783
+ for (const [parameterIndex, param] of params.entries()) {
45784
+ const parameterBinding = parameterIndex === 0 ? resolveFirstArgumentBinding(param) : param;
45785
+ if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
45786
+ }
45787
+ const propsParam = resolveFirstArgumentBinding(params[0]);
45280
45788
  const body = functionNode.body;
45281
45789
  if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
45282
45790
  if (!isNodeOfType(body, "BlockStatement")) return bindings;
@@ -45782,8 +46290,8 @@ const rnAnimationReactionAsDerived = defineRule({
45782
46290
  if (statements.length !== 1) return;
45783
46291
  const onlyStatement = statements[0];
45784
46292
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
45785
- singleAssignment = onlyStatement.expression;
45786
- } else if (body) singleAssignment = body;
46293
+ singleAssignment = unwrapDiscardedExpression(onlyStatement);
46294
+ } else if (body) singleAssignment = unwrapDiscardedExpression(body);
45787
46295
  if (!singleAssignment) return;
45788
46296
  const isValueAssignment = isNodeOfType(singleAssignment, "AssignmentExpression") && isNodeOfType(singleAssignment.left, "MemberExpression") && isNodeOfType(singleAssignment.left.object, "Identifier") && isNodeOfType(singleAssignment.left.property, "Identifier") && singleAssignment.left.property.name === "value";
45789
46297
  const isSetCall = isNodeOfType(singleAssignment, "CallExpression") && isNodeOfType(singleAssignment.callee, "MemberExpression") && isNodeOfType(singleAssignment.callee.property, "Identifier") && singleAssignment.callee.property.name === "set" && (singleAssignment.arguments?.length ?? 0) === 1;
@@ -52703,7 +53211,8 @@ const serverCacheWithObjectLiteral = defineRule({
52703
53211
  if (!isNodeOfType(node.callee, "Identifier")) return;
52704
53212
  if (!cachedFunctionNames.has(node.callee.name)) return;
52705
53213
  const firstArg = node.arguments?.[0];
52706
- if (!isNodeOfType(firstArg, "ObjectExpression")) return;
53214
+ if (!firstArg || isNodeOfType(firstArg, "SpreadElement")) return;
53215
+ if (!isNodeOfType(unwrapObjectIntegrityExpression(firstArg, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES), "ObjectExpression")) return;
52707
53216
  context.report({
52708
53217
  node,
52709
53218
  message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
@@ -53390,10 +53899,6 @@ const isInvalidStyleExpression = (expression) => {
53390
53899
  }
53391
53900
  return false;
53392
53901
  };
53393
- const getJsxOpeningElementName = (node) => {
53394
- if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
53395
- return null;
53396
- };
53397
53902
  const stylePropObject = defineRule({
53398
53903
  id: "style-prop-object",
53399
53904
  title: "Style prop is not an object",
@@ -53405,7 +53910,8 @@ const stylePropObject = defineRule({
53405
53910
  const allowSet = new Set(allow);
53406
53911
  return {
53407
53912
  JSXOpeningElement(node) {
53408
- const elementName = getJsxOpeningElementName(node);
53913
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
53914
+ const elementName = resolveJsxElementType(node);
53409
53915
  if (elementName && allowSet.has(elementName)) return;
53410
53916
  if (elementName) {
53411
53917
  const firstCharCode = elementName.charCodeAt(0);
@@ -54110,7 +54616,7 @@ const tanstackStartNoAnchorElement = defineRule({
54110
54616
  create: (context) => {
54111
54617
  if (!isInProjectDirectory(context, "routes")) return {};
54112
54618
  return { JSXOpeningElement(node) {
54113
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
54619
+ if (resolveJsxElementType(node) !== "a") return;
54114
54620
  const attributes = node.attributes ?? [];
54115
54621
  const hrefAttribute = findJsxAttribute(attributes, "href");
54116
54622
  if (!hrefAttribute?.value) return;
@@ -54731,8 +55237,7 @@ const voidDomElementsNoChildren = defineRule({
54731
55237
  create: (context) => ({
54732
55238
  JSXElement(node) {
54733
55239
  const openingElement = node.openingElement;
54734
- if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
54735
- const tagName = openingElement.name.name;
55240
+ const tagName = resolveJsxElementType(openingElement);
54736
55241
  if (!VOID_DOM_ELEMENTS.has(tagName)) return;
54737
55242
  const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
54738
55243
  const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);