oxlint-plugin-react-doctor 0.7.5 → 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 +1744 -817
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,14 +1,11 @@
1
+ import { KEYS } from "eslint-visitor-keys";
1
2
  import * as path from "node:path";
2
3
  import * as fs from "node:fs";
3
4
  import { readFileSync } from "node:fs";
4
- import { parseSync } from "oxc-parser";
5
+ import { parseSync, visitorKeys } from "oxc-parser";
5
6
  import { analyze } from "eslint-scope";
6
- import * as eslintVisitorKeys from "eslint-visitor-keys";
7
- //#region src/plugin/utils/has-type-property.ts
8
- const hasTypeProperty = (value) => Boolean(value && typeof value === "object" && "type" in value);
9
- //#endregion
10
7
  //#region src/plugin/utils/is-node-of-type.ts
11
- const isNodeOfType = (node, type) => Boolean(hasTypeProperty(node) && node.type === type);
8
+ const isNodeOfType = (node, type) => node !== null && typeof node === "object" && "type" in node && node.type === type;
12
9
  //#endregion
13
10
  //#region src/plugin/utils/non-react-jsx-dialect.ts
14
11
  const NON_REACT_JSX_DIALECT_PACKAGES = new Set([
@@ -400,6 +397,7 @@ const isProductionFilePath = (relativePath, sourceFilePattern) => {
400
397
  //#endregion
401
398
  //#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
402
399
  const isProductionSourcePath = (relativePath) => {
400
+ if (/\.d\.[cm]?[jt]s$/i.test(relativePath)) return false;
403
401
  return isProductionFilePath(relativePath, SOURCE_FILE_PATTERN);
404
402
  };
405
403
  //#endregion
@@ -565,7 +563,8 @@ const REACT_RUNTIME_MODULE_SOURCES = new Set([
565
563
  "react",
566
564
  "react-dom",
567
565
  "preact/compat",
568
- "preact/hooks"
566
+ "preact/hooks",
567
+ "@wordpress/element"
569
568
  ]);
570
569
  const REACT_ECOSYSTEM_PACKAGE_NAMES = new Set([
571
570
  "next",
@@ -802,24 +801,55 @@ const isHookCall$2 = (node, hookName) => {
802
801
  };
803
802
  //#endregion
804
803
  //#region src/plugin/utils/is-ast-node.ts
805
- const isAstNode = (value) => {
806
- if (!hasTypeProperty(value)) return false;
807
- return typeof value.type === "string";
804
+ const isAstNode = (value) => value !== null && typeof value === "object" && "type" in value && typeof value.type === "string";
805
+ //#endregion
806
+ //#region src/plugin/utils/runtime-visitor-keys.ts
807
+ const RUNTIME_VISITOR_KEYS = {
808
+ ...KEYS,
809
+ ArrayPattern: ["decorators", ...KEYS.ArrayPattern],
810
+ AssignmentPattern: ["decorators", ...KEYS.AssignmentPattern],
811
+ ClassDeclaration: ["decorators", ...KEYS.ClassDeclaration],
812
+ ClassExpression: ["decorators", ...KEYS.ClassExpression],
813
+ Identifier: ["decorators", ...KEYS.Identifier],
814
+ MethodDefinition: ["decorators", ...KEYS.MethodDefinition],
815
+ ObjectPattern: ["decorators", ...KEYS.ObjectPattern],
816
+ PropertyDefinition: ["decorators", ...KEYS.PropertyDefinition],
817
+ RestElement: ["decorators", ...KEYS.RestElement]
808
818
  };
809
819
  //#endregion
810
820
  //#region src/plugin/utils/walk-ast.ts
811
- const walkAst = (node, visitor) => {
812
- if (!node || typeof node !== "object") return;
813
- if (visitor(node) === false) return;
821
+ const forEachChildNode = (node, visit) => {
814
822
  const nodeRecord = node;
815
- for (const key of Object.keys(nodeRecord)) {
816
- if (key === "parent") continue;
823
+ const childKeys = RUNTIME_VISITOR_KEYS[node.type];
824
+ if (childKeys !== void 0) {
825
+ for (let keyIndex = 0; keyIndex < childKeys.length; keyIndex += 1) {
826
+ const child = nodeRecord[childKeys[keyIndex]];
827
+ if (Array.isArray(child)) for (let itemIndex = 0; itemIndex < child.length; itemIndex += 1) {
828
+ const item = child[itemIndex];
829
+ if (isAstNode(item)) visit(item);
830
+ }
831
+ else if (isAstNode(child)) visit(child);
832
+ }
833
+ return;
834
+ }
835
+ for (const key in nodeRecord) {
836
+ if (key === "parent" || !Object.hasOwn(nodeRecord, key)) continue;
817
837
  const child = nodeRecord[key];
818
- if (Array.isArray(child)) {
819
- for (const item of child) if (isAstNode(item)) walkAst(item, visitor);
820
- } else if (isAstNode(child)) walkAst(child, visitor);
838
+ if (Array.isArray(child)) for (let itemIndex = 0; itemIndex < child.length; itemIndex += 1) {
839
+ const item = child[itemIndex];
840
+ if (isAstNode(item)) visit(item);
841
+ }
842
+ else if (isAstNode(child)) visit(child);
821
843
  }
822
844
  };
845
+ const walkAst = (node, visitor) => {
846
+ if (!node || typeof node !== "object") return;
847
+ const visitNode = (current) => {
848
+ if (visitor(current) === false) return;
849
+ forEachChildNode(current, visitNode);
850
+ };
851
+ visitNode(node);
852
+ };
823
853
  //#endregion
824
854
  //#region src/plugin/rules/state-and-effects/activity-wraps-effect-heavy-subtree.ts
825
855
  const ACTIVITY_IMPORTED_NAMES = new Set(["Activity", "unstable_Activity"]);
@@ -1262,7 +1292,7 @@ const REGEX_PRECEDING_KEYWORDS = new Set([
1262
1292
  "await",
1263
1293
  "throw"
1264
1294
  ]);
1265
- const isRegexLiteralStart = (characters, slashIndex) => {
1295
+ const isRegexLiteralStart = (content, characters, slashIndex) => {
1266
1296
  let cursor = slashIndex - 1;
1267
1297
  while (cursor >= 0 && WHITESPACE_PATTERN.test(characters[cursor])) cursor -= 1;
1268
1298
  if (cursor < 0) return true;
@@ -1272,7 +1302,7 @@ const isRegexLiteralStart = (characters, slashIndex) => {
1272
1302
  let wordStartIndex = cursor;
1273
1303
  while (wordStartIndex > 0 && IDENTIFIER_CHARACTER_PATTERN.test(characters[wordStartIndex - 1])) wordStartIndex -= 1;
1274
1304
  if (wordStartIndex > 0 && characters[wordStartIndex - 1] === ".") return false;
1275
- return REGEX_PRECEDING_KEYWORDS.has(characters.slice(wordStartIndex, cursor + 1).join(""));
1305
+ return REGEX_PRECEDING_KEYWORDS.has(content.slice(wordStartIndex, cursor + 1));
1276
1306
  }
1277
1307
  if (previousCharacter === "<") return false;
1278
1308
  if (previousCharacter === ">") return characterBefore === "=";
@@ -1313,13 +1343,15 @@ const quotedLiteralHasWhitespace = (content, openQuoteIndex, delimiter) => {
1313
1343
  return false;
1314
1344
  };
1315
1345
  const blankNonCodePreservingPositions = (content, blankStringContents) => {
1316
- const characters = content.split("");
1346
+ let characters = null;
1317
1347
  let stringDelimiter = null;
1318
1348
  let isBlankingString = false;
1319
1349
  const templateExpressionDepths = [];
1320
1350
  let index = 0;
1321
1351
  const blankUnlessNewline = (offset) => {
1322
- if (offset < content.length && content[offset] !== "\n") characters[offset] = " ";
1352
+ if (offset >= content.length || content[offset] === "\n") return;
1353
+ characters ??= content.split("");
1354
+ characters[offset] = " ";
1323
1355
  };
1324
1356
  while (index < content.length) {
1325
1357
  const character = content[index];
@@ -1366,6 +1398,7 @@ const blankNonCodePreservingPositions = (content, blankStringContents) => {
1366
1398
  continue;
1367
1399
  }
1368
1400
  if (character === "/" && nextCharacter === "/") {
1401
+ characters ??= content.split("");
1369
1402
  while (index < content.length && content[index] !== "\n") {
1370
1403
  characters[index] = " ";
1371
1404
  index += 1;
@@ -1373,6 +1406,7 @@ const blankNonCodePreservingPositions = (content, blankStringContents) => {
1373
1406
  continue;
1374
1407
  }
1375
1408
  if (character === "/" && nextCharacter === "*") {
1409
+ characters ??= content.split("");
1376
1410
  while (index < content.length) {
1377
1411
  if (content[index] === "*" && content[index + 1] === "/") {
1378
1412
  characters[index] = " ";
@@ -1386,7 +1420,7 @@ const blankNonCodePreservingPositions = (content, blankStringContents) => {
1386
1420
  continue;
1387
1421
  }
1388
1422
  if (character === "/") {
1389
- const regexEndIndex = isRegexLiteralStart(characters, index) ? findRegexLiteralEnd(content, index) : null;
1423
+ const regexEndIndex = isRegexLiteralStart(content, characters ?? content, index) ? findRegexLiteralEnd(content, index) : null;
1390
1424
  if (regexEndIndex !== null) {
1391
1425
  if (blankStringContents) for (let interiorIndex = index + 1; interiorIndex < regexEndIndex - 1; interiorIndex += 1) blankUnlessNewline(interiorIndex);
1392
1426
  index = regexEndIndex;
@@ -1404,9 +1438,9 @@ const blankNonCodePreservingPositions = (content, blankStringContents) => {
1404
1438
  }
1405
1439
  index += 1;
1406
1440
  }
1407
- return characters.join("");
1441
+ return characters?.join("") ?? content;
1408
1442
  };
1409
- const stripCommentsPreservingPositions = (content) => blankNonCodePreservingPositions(content, false);
1443
+ const stripCommentsPreservingPositions = (content) => content.includes("//") || content.includes("/*") ? blankNonCodePreservingPositions(content, false) : content;
1410
1444
  const stripCommentsAndStringLiteralsPreservingPositions = (content) => blankNonCodePreservingPositions(content, true);
1411
1445
  //#endregion
1412
1446
  //#region src/plugin/rules/security-scan/utils/scan-by-pattern.ts
@@ -1425,10 +1459,15 @@ const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, ignoreSt
1425
1459
  if (!shouldScan(file)) return [];
1426
1460
  const content = getScannableContent(file, ignoreStringLiterals);
1427
1461
  if (requireAll !== void 0 && !requireAll.every((gate) => gate.test(content))) return [];
1428
- const matchedPattern = (pattern instanceof RegExp ? [pattern] : pattern).find((candidate) => candidate.test(content));
1429
- if (matchedPattern === void 0) return [];
1462
+ let matchIndex = -1;
1463
+ if (pattern instanceof RegExp) matchIndex = content.search(pattern);
1464
+ else for (let patternIndex = 0; patternIndex < pattern.length; patternIndex += 1) {
1465
+ matchIndex = content.search(pattern[patternIndex]);
1466
+ if (matchIndex >= 0) break;
1467
+ }
1468
+ if (matchIndex < 0) return [];
1430
1469
  if (suppressWhen !== void 0 && suppressWhen.test(content)) return [];
1431
- const { line, column } = getMatchLocation(content, matchedPattern);
1470
+ const { line, column } = getLocationAtIndex(content, matchIndex);
1432
1471
  return [{
1433
1472
  message,
1434
1473
  line,
@@ -1438,6 +1477,7 @@ const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, ignoreSt
1438
1477
  //#endregion
1439
1478
  //#region src/plugin/rules/security-scan/agent-tool-capability-risk.ts
1440
1479
  const AGENT_TOOL_DEFINITION_PATTERN = /\b(?:tool\s*\(\s*\{|createTool\s*\(|defineTool\s*\(|new\s+(?:DynamicTool|StructuredTool)\s*\()/;
1480
+ const AGENT_TOOL_DEFINITION_PREFILTER_PATTERN = /\b(?:tool|createTool|defineTool|DynamicTool|StructuredTool)\b/;
1441
1481
  const AGENT_TOOL_CONTEXT_PATH_PATTERN = /(?:^|\/)(?:agents?|tools?|mcp)(?:\/|$)|(?:agent|tool|mcp)[^/]*\.[cm]?[jt]sx?$/i;
1442
1482
  const agentToolCapabilityRisk = defineRule({
1443
1483
  id: "agent-tool-capability-risk",
@@ -1445,7 +1485,7 @@ const agentToolCapabilityRisk = defineRule({
1445
1485
  severity: "warn",
1446
1486
  recommendation: "Treat tool inputs as prompt-injection controlled. Validate arguments, scope permissions per call, and avoid exposing shell/file/network primitives directly to agents.",
1447
1487
  scan: scanByPattern({
1448
- shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath),
1488
+ shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath) && AGENT_TOOL_DEFINITION_PREFILTER_PATTERN.test(file.content) && AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN.test(file.content),
1449
1489
  pattern: AGENT_TOOL_DEFINITION_PATTERN,
1450
1490
  requireAll: [AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
1451
1491
  ignoreStringLiterals: true,
@@ -1478,6 +1518,37 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
1478
1518
  }
1479
1519
  };
1480
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
1481
1552
  //#region src/plugin/utils/get-element-type.ts
1482
1553
  const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
1483
1554
  const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
@@ -1490,14 +1561,8 @@ const readJsxA11ySettings = (settings) => {
1490
1561
  jsxA11ySettingsCache.set(settings, a11ySettings);
1491
1562
  return a11ySettings;
1492
1563
  };
1493
- const flattenJsxName$2 = (name) => {
1494
- if (isNodeOfType(name, "JSXIdentifier")) return name.name;
1495
- if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
1496
- if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
1497
- return "";
1498
- };
1499
1564
  const computeElementType = (openingElement, a11ySettings) => {
1500
- const baseName = flattenJsxName$2(openingElement.name);
1565
+ const baseName = resolveJsxElementType(openingElement);
1501
1566
  if (a11ySettings.polymorphicPropName) {
1502
1567
  const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
1503
1568
  if (polymorphicAttribute) {
@@ -2332,8 +2397,9 @@ const anchorIsValid = defineRule({
2332
2397
  const isTestlikeFile = isTestlikeFilename(context.filename);
2333
2398
  return { JSXOpeningElement(node) {
2334
2399
  if (isTestlikeFile) return;
2335
- if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
2336
- 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;
2337
2403
  let hrefAttribute;
2338
2404
  for (const attributeName of settings.hrefAttributeNames) {
2339
2405
  hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
@@ -5139,8 +5205,10 @@ const STORAGE_GLOBALS = new Set([
5139
5205
  const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
5140
5206
  const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
5141
5207
  const STRONG_AUTH_KEY_PATTERN = /jwt|secret|password|passwd|credential|private[-_]?key|api[-_]?key|bearer|access[-_]?token|refresh[-_]?token|auth[-_]?token|id[-_]?token|session/i;
5208
+ const PRODUCT_API_KEY_RECORDS_PATTERN = /(?:^|[._:-])(?:created|saved|integration|mailing)[-_]?api[-_]?keys$/i;
5142
5209
  const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
5143
5210
  const isAuthCredentialKey = (key) => {
5211
+ if (PRODUCT_API_KEY_RECORDS_PATTERN.test(key)) return false;
5144
5212
  if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
5145
5213
  if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
5146
5214
  if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
@@ -5686,7 +5754,7 @@ const buttonHasType = defineRule({
5686
5754
  return {
5687
5755
  JSXOpeningElement(node) {
5688
5756
  if (isTestlikeFile) return;
5689
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "button") return;
5757
+ if (resolveJsxElementType(node) !== "button") return;
5690
5758
  const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
5691
5759
  if (!typeAttr) {
5692
5760
  if (hasJsxSpreadAttribute$1(node.attributes)) {
@@ -5876,7 +5944,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
5876
5944
  };
5877
5945
  return {
5878
5946
  JSXOpeningElement(node) {
5879
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
5947
+ if (resolveJsxElementType(node) !== "input") return;
5880
5948
  reportFromPresence(collectFromJsxAttributes(node.attributes));
5881
5949
  },
5882
5950
  CallExpression(node) {
@@ -5892,6 +5960,21 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
5892
5960
  }
5893
5961
  });
5894
5962
  //#endregion
5963
+ //#region src/plugin/utils/get-static-property-key-name.ts
5964
+ const getStaticPropertyKeyName = (node, options = {}) => {
5965
+ if (!isNodeOfType(node, "Property")) return null;
5966
+ if (node.computed) {
5967
+ if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
5968
+ return null;
5969
+ }
5970
+ if (isNodeOfType(node.key, "Identifier")) return node.key.name;
5971
+ if (isNodeOfType(node.key, "Literal")) {
5972
+ if (typeof node.key.value === "string") return node.key.value;
5973
+ if (options.stringifyNonStringLiterals) return String(node.key.value);
5974
+ }
5975
+ return null;
5976
+ };
5977
+ //#endregion
5895
5978
  //#region src/plugin/utils/is-presentation-role.ts
5896
5979
  const isPresentationRole = (openingElement) => {
5897
5980
  const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
@@ -5936,6 +6019,21 @@ const isPureEventBlockerHandler = (attribute) => {
5936
6019
  return false;
5937
6020
  };
5938
6021
  //#endregion
6022
+ //#region src/plugin/utils/resolve-const-identifier-alias.ts
6023
+ const resolveConstIdentifierAlias = (identifier, scopes) => {
6024
+ if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
6025
+ const visitedSymbolIds = /* @__PURE__ */ new Set();
6026
+ let symbol = scopes.symbolFor(identifier);
6027
+ while (symbol?.kind === "const") {
6028
+ if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
6029
+ visitedSymbolIds.add(symbol.id);
6030
+ const initializer = stripParenExpression(symbol.initializer);
6031
+ if (!isNodeOfType(initializer, "Identifier")) return symbol;
6032
+ symbol = scopes.symbolFor(initializer);
6033
+ }
6034
+ return symbol;
6035
+ };
6036
+ //#endregion
5939
6037
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
5940
6038
  const MESSAGE$57 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
5941
6039
  const KEY_HANDLERS = [
@@ -5946,6 +6044,63 @@ const KEY_HANDLERS = [
5946
6044
  "onKeyDownCapture",
5947
6045
  "onKeyPressCapture"
5948
6046
  ];
6047
+ const CLICK_HANDLERS = ["onClick", "onClickCapture"];
6048
+ const TRANSPARENT_SPREAD_EVENT_NAMES = new Set([...CLICK_HANDLERS, ...KEY_HANDLERS].map((eventName) => eventName.toLowerCase()));
6049
+ const CONSERVATIVE_SPREAD_PROP_NAMES = new Set([
6050
+ "aria-hidden",
6051
+ "onmouseenter",
6052
+ "onmouseover",
6053
+ "role"
6054
+ ]);
6055
+ const resolveSpreadObjectExpression = (expression, scopes) => {
6056
+ const innerExpression = stripParenExpression(expression);
6057
+ if (isNodeOfType(innerExpression, "ObjectExpression")) return innerExpression;
6058
+ if (!isNodeOfType(innerExpression, "Identifier")) return null;
6059
+ const symbol = resolveConstIdentifierAlias(innerExpression, scopes);
6060
+ if (symbol?.kind !== "const" || !symbol.initializer) return null;
6061
+ const initializer = stripParenExpression(symbol.initializer);
6062
+ return isNodeOfType(initializer, "ObjectExpression") ? initializer : null;
6063
+ };
6064
+ const collectTransparentSpreadEventNames = (expression, scopes, eventValues, visitedObjectExpressions) => {
6065
+ const objectExpression = resolveSpreadObjectExpression(expression, scopes);
6066
+ if (!objectExpression || visitedObjectExpressions.has(objectExpression)) return false;
6067
+ visitedObjectExpressions.add(objectExpression);
6068
+ let isTransparent = true;
6069
+ for (const property of objectExpression.properties) {
6070
+ if (isNodeOfType(property, "SpreadElement")) {
6071
+ if (!collectTransparentSpreadEventNames(property.argument, scopes, eventValues, visitedObjectExpressions)) {
6072
+ isTransparent = false;
6073
+ break;
6074
+ }
6075
+ continue;
6076
+ }
6077
+ if (!isNodeOfType(property, "Property")) {
6078
+ isTransparent = false;
6079
+ break;
6080
+ }
6081
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
6082
+ if (!propertyName) {
6083
+ isTransparent = false;
6084
+ break;
6085
+ }
6086
+ const normalizedPropertyName = propertyName.toLowerCase();
6087
+ if (CONSERVATIVE_SPREAD_PROP_NAMES.has(normalizedPropertyName)) {
6088
+ isTransparent = false;
6089
+ break;
6090
+ }
6091
+ if (TRANSPARENT_SPREAD_EVENT_NAMES.has(normalizedPropertyName)) eventValues.set(normalizedPropertyName, property.value);
6092
+ }
6093
+ visitedObjectExpressions.delete(objectExpression);
6094
+ return isTransparent;
6095
+ };
6096
+ const getTransparentSpreadEventValues = (attributes, scopes) => {
6097
+ const eventValues = /* @__PURE__ */ new Map();
6098
+ for (const attribute of attributes) {
6099
+ if (!isNodeOfType(attribute, "JSXSpreadAttribute")) continue;
6100
+ if (!collectTransparentSpreadEventNames(attribute.argument, scopes, eventValues, /* @__PURE__ */ new Set())) return null;
6101
+ }
6102
+ return eventValues;
6103
+ };
5949
6104
  const FOCUSLESS_CONTAINER_TAGS = new Set([
5950
6105
  "tr",
5951
6106
  "td",
@@ -5993,7 +6148,10 @@ const isFocusForwardingFunctionBody = (body) => {
5993
6148
  };
5994
6149
  const resolveHandlerFunction = (attribute) => {
5995
6150
  if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
5996
- let expression = attribute.value.expression;
6151
+ return resolveHandlerFunctionExpression(attribute.value.expression);
6152
+ };
6153
+ const resolveHandlerFunctionExpression = (handlerExpression) => {
6154
+ let expression = handlerExpression;
5997
6155
  if (isNodeOfType(expression, "Identifier")) {
5998
6156
  const binding = findVariableInitializer(expression, expression.name);
5999
6157
  if (!binding?.initializer) return null;
@@ -6092,18 +6250,22 @@ const clickEventsHaveKeyEvents = defineRule({
6092
6250
  if (!HTML_TAGS.has(tag)) return;
6093
6251
  if (tag === "label") return;
6094
6252
  if (!FOCUSLESS_CONTAINER_TAGS.has(tag) && isInteractiveElement(tag, node)) return;
6253
+ const spreadEventValues = getTransparentSpreadEventValues(node.attributes, context.scopes);
6254
+ if (!spreadEventValues) return;
6095
6255
  const onClick = hasJsxPropIgnoreCase(node.attributes, "onClick") ?? hasJsxPropIgnoreCase(node.attributes, "onClickCapture");
6096
- if (!onClick) return;
6097
- if (isPureEventBlockerHandler(onClick)) return;
6098
- if (isFocusForwardingHandler(onClick)) return;
6099
- if (hasJsxSpreadAttribute$1(node.attributes)) return;
6256
+ const spreadOnClickExpression = CLICK_HANDLERS.map((name) => spreadEventValues.get(name.toLowerCase())).find((expression) => expression !== void 0);
6257
+ if (!onClick && !spreadOnClickExpression) return;
6258
+ if (onClick && isPureEventBlockerHandler(onClick)) return;
6259
+ if (onClick && isFocusForwardingHandler(onClick)) return;
6260
+ const spreadHandlerFunction = spreadOnClickExpression ? resolveHandlerFunctionExpression(spreadOnClickExpression) : null;
6261
+ if (spreadHandlerFunction && (isFocusForwardingFunctionBody(spreadHandlerFunction.body ?? null) || containsBackdropDismissComparison(spreadHandlerFunction.body ?? null))) return;
6100
6262
  if (hasCompositeItemRole(node)) return;
6101
6263
  if (isHoverSelectionListItem(tag, node)) return;
6102
- if (isBackdropDismissHandler(onClick)) return;
6264
+ if (onClick && isBackdropDismissHandler(onClick)) return;
6103
6265
  if (containsKeyboardActivatableDescendant(node.parent)) return;
6104
6266
  if (isHiddenFromScreenReader(node, context.settings)) return;
6105
6267
  if (isPresentationRole(node)) return;
6106
- if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
6268
+ if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler) || spreadEventValues.has(handler.toLowerCase()))) return;
6107
6269
  context.report({
6108
6270
  node: node.name,
6109
6271
  message: MESSAGE$57
@@ -6134,6 +6296,131 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
6134
6296
  return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
6135
6297
  };
6136
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
6137
6424
  //#region src/plugin/rules/client/client-localstorage-no-version.ts
6138
6425
  const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
6139
6426
  const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
@@ -6155,26 +6442,39 @@ const clientLocalstorageNoVersion = defineRule({
6155
6442
  severity: "warn",
6156
6443
  category: "Correctness",
6157
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.",
6158
- create: (context) => ({ CallExpression(node) {
6159
- if (!isNodeOfType(node.callee, "MemberExpression")) return;
6160
- const receiver = stripParenExpression(node.callee.object);
6161
- if (!isNodeOfType(receiver, "Identifier")) return;
6162
- if (receiver.name !== "localStorage") return;
6163
- if (!isNodeOfType(node.callee.property, "Identifier")) return;
6164
- if (node.callee.property.name !== "setItem") return;
6165
- const keyArg = node.arguments?.[0];
6166
- if (!keyArg) return;
6167
- const keyValue = resolveStringKey(keyArg, context);
6168
- if (keyValue === null) return;
6169
- if (isVersionedKey(keyValue)) return;
6170
- const valueArg = node.arguments?.[1];
6171
- if (!valueArg) return;
6172
- if (!isJsonStringifyCall(valueArg)) return;
6173
- context.report({
6174
- node: keyArg,
6175
- 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").`
6176
- });
6177
- } })
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
+ }
6178
6478
  });
6179
6479
  //#endregion
6180
6480
  //#region src/plugin/rules/client/client-passive-event-listeners.ts
@@ -6793,6 +7093,30 @@ const corsCookieTrustRisk = defineRule({
6793
7093
  })
6794
7094
  });
6795
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
6796
7120
  //#region src/plugin/rules/security-scan/dangerous-html-sink.ts
6797
7121
  const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
6798
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]*)/;
@@ -6808,6 +7132,7 @@ const I18N_VALUE_PATTERN = /\b(?:t|i18n|translate|formatMessage|intl)\s*[.(]/;
6808
7132
  const ESCAPING_SERIALIZER_CALL_PATTERN = /^(?:[\w$.]+\.)?(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*)\s*\(/;
6809
7133
  const HIGHLIGHTER_LIBRARY_PATTERN = /\b(?:shiki|prism|hljs|highlightjs|getHighlighter|codeToHtml|codeToHast|refractor|lowlight|starry-night)\b|highlight\.js/i;
6810
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;
6811
7136
  const BARE_IDENTIFIER_VALUE_PATTERN = /^[\w$]+\s*(?:[;,})\n]|$)/;
6812
7137
  const IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN = /^[\w$]+\s*(?:\|\||\?\?)\s*(?:"[^"\n]*"|'[^'\n]*'|`[^`$]*`)\s*(?:[;,})\n]|$)/;
6813
7138
  const MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN = /^[\w$]+(?:\.[\w$]+|\[[^\]]*\])+\s*(?:[;,})\n]|$)/;
@@ -6829,11 +7154,10 @@ const getTemplateInterpolations = (valueTail) => {
6829
7154
  const interpolations = valueTail.slice(1, closingBacktickIndex).match(/\$\{[^}]*\}/g);
6830
7155
  return interpolations === null ? "" : interpolations.join(" ");
6831
7156
  };
6832
- const getIdentifierDeclarationInitializer = (identifier, fileContent) => {
6833
- const declarationMatch = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]{0,120})?=\\s*`).exec(fileContent);
6834
- if (declarationMatch === null) return null;
6835
- const initializerStartIndex = declarationMatch.index + declarationMatch[0].length;
6836
- return fileContent.slice(initializerStartIndex, initializerStartIndex + STATIC_TEMPLATE_MAX_CHARS);
7157
+ const getIdentifierDeclarationInitializer = (identifier, sinkIndex, fileContent) => {
7158
+ const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
7159
+ if (declaration === null) return null;
7160
+ return fileContent.slice(declaration.initializerStartIndex, declaration.initializerStartIndex + STATIC_TEMPLATE_MAX_CHARS);
6837
7161
  };
6838
7162
  const isPureStringLiteralConcat = (initializerText) => {
6839
7163
  if (!/^["']/.test(initializerText)) return false;
@@ -6903,6 +7227,229 @@ const isAllOperandsDomContentConcat = (valueExpression) => {
6903
7227
  return !HTML_TAINT_PATTERN.test(withoutCast.replace(DOM_CONTENT_SOURCE_VALUE_PATTERN, ""));
6904
7228
  });
6905
7229
  };
7230
+ const findMatchingBraceIndex = (fileContent, openingBraceIndex) => {
7231
+ let depth = 0;
7232
+ let quote = null;
7233
+ for (let index = openingBraceIndex; index < fileContent.length; index += 1) {
7234
+ const character = fileContent[index];
7235
+ if (quote !== null) {
7236
+ if (character === quote && fileContent[index - 1] !== "\\") quote = null;
7237
+ continue;
7238
+ }
7239
+ if (character === "\"" || character === "'" || character === "`") {
7240
+ quote = character;
7241
+ continue;
7242
+ }
7243
+ if (character === "{") depth += 1;
7244
+ if (character === "}") {
7245
+ depth -= 1;
7246
+ if (depth === 0) return index;
7247
+ }
7248
+ }
7249
+ return fileContent.length;
7250
+ };
7251
+ const findContainingBlockEndIndex = (fileContent, targetIndex) => {
7252
+ const openingBraceIndexes = [];
7253
+ let quote = null;
7254
+ let isLineComment = false;
7255
+ let isBlockComment = false;
7256
+ for (let index = 0; index < targetIndex; index += 1) {
7257
+ const character = fileContent[index];
7258
+ const nextCharacter = fileContent[index + 1];
7259
+ if (isLineComment) {
7260
+ if (character === "\n") isLineComment = false;
7261
+ continue;
7262
+ }
7263
+ if (isBlockComment) {
7264
+ if (character === "*" && nextCharacter === "/") {
7265
+ isBlockComment = false;
7266
+ index += 1;
7267
+ }
7268
+ continue;
7269
+ }
7270
+ if (quote !== null) {
7271
+ if (character === quote && fileContent[index - 1] !== "\\") quote = null;
7272
+ continue;
7273
+ }
7274
+ if (character === "/" && nextCharacter === "/") {
7275
+ isLineComment = true;
7276
+ index += 1;
7277
+ continue;
7278
+ }
7279
+ if (character === "/" && nextCharacter === "*") {
7280
+ isBlockComment = true;
7281
+ index += 1;
7282
+ continue;
7283
+ }
7284
+ if (character === "\"" || character === "'" || character === "`") {
7285
+ quote = character;
7286
+ continue;
7287
+ }
7288
+ if (character === "{") openingBraceIndexes.push(index);
7289
+ if (character === "}") openingBraceIndexes.pop();
7290
+ }
7291
+ const openingBraceIndex = openingBraceIndexes.at(-1);
7292
+ return openingBraceIndex === void 0 ? fileContent.length : findMatchingBraceIndex(fileContent, openingBraceIndex);
7293
+ };
7294
+ const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) => {
7295
+ const initializerPattern = new RegExp(`(const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
7296
+ let nearestDeclaration = null;
7297
+ let nearestDeclarationIndex = -1;
7298
+ for (const match of fileContent.matchAll(initializerPattern)) {
7299
+ const declarationIndex = match.index;
7300
+ if (declarationIndex === void 0 || declarationIndex >= sinkIndex || declarationIndex <= nearestDeclarationIndex || findContainingBlockEndIndex(fileContent, declarationIndex) < sinkIndex) continue;
7301
+ nearestDeclarationIndex = declarationIndex;
7302
+ const initializer = match[2];
7303
+ if (initializer === void 0) continue;
7304
+ nearestDeclaration = {
7305
+ declarationIndex,
7306
+ initializer,
7307
+ initializerStartIndex: declarationIndex + match[0].length - initializer.length,
7308
+ isImmutable: match[1] === "const"
7309
+ };
7310
+ }
7311
+ return nearestDeclaration;
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
+ };
7355
+ const findContainingFunctionParameterSource = (identifier, sinkIndex, fileContent) => {
7356
+ const patterns = [/function\s+([\w$]+)\s*\(([^)]*)\)\s*\{/g, /(?:const|let|var)\s+([\w$]+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>\s*\{/g];
7357
+ let closestSource = null;
7358
+ let closestStartIndex = -1;
7359
+ for (const pattern of patterns) for (const match of fileContent.matchAll(pattern)) {
7360
+ const matchIndex = match.index;
7361
+ if (matchIndex === void 0 || matchIndex >= sinkIndex || matchIndex < closestStartIndex) continue;
7362
+ const bodyEndIndex = findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{"));
7363
+ if (bodyEndIndex < sinkIndex) continue;
7364
+ const parameterIndex = (match[2] ?? "").split(",").findIndex((parameter) => parameter.trim().match(/^(?:\.\.\.)?([\w$]+)/)?.[1] === identifier);
7365
+ if (parameterIndex < 0) continue;
7366
+ closestStartIndex = matchIndex;
7367
+ const functionName = match[1] ?? "";
7368
+ closestSource = {
7369
+ bodyEndIndex,
7370
+ declarationNameIndex: matchIndex + match[0].indexOf(functionName),
7371
+ functionName,
7372
+ parameterIndex
7373
+ };
7374
+ }
7375
+ return closestSource;
7376
+ };
7377
+ const splitTopLevelArguments = (argumentText) => {
7378
+ const argumentsList = [];
7379
+ let startIndex = 0;
7380
+ let depth = 0;
7381
+ let quote = null;
7382
+ for (let index = 0; index < argumentText.length; index += 1) {
7383
+ const character = argumentText[index];
7384
+ if (quote !== null) {
7385
+ if (character === quote && argumentText[index - 1] !== "\\") quote = null;
7386
+ continue;
7387
+ }
7388
+ if (character === "\"" || character === "'" || character === "`") {
7389
+ quote = character;
7390
+ continue;
7391
+ }
7392
+ if (character === "(" || character === "[" || character === "{") depth += 1;
7393
+ if (character === ")" || character === "]" || character === "}") depth -= 1;
7394
+ if (character === "," && depth === 0) {
7395
+ argumentsList.push(argumentText.slice(startIndex, index).trim());
7396
+ startIndex = index + 1;
7397
+ }
7398
+ }
7399
+ argumentsList.push(argumentText.slice(startIndex).trim());
7400
+ return argumentsList;
7401
+ };
7402
+ const isBackedByFunctionParameter = (expression, expressionIndex, fileContent, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
7403
+ const identifier = expression.trim().match(/^([\w$]+)$/)?.[1];
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;
7418
+ visitedIdentifiers.add(identifier);
7419
+ const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
7420
+ const parameterSource = findContainingFunctionParameterSource(identifier, sinkIndex, fileContent);
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);
7452
+ };
6906
7453
  const dangerousHtmlSink = defineRule({
6907
7454
  id: "dangerous-html-sink",
6908
7455
  title: "HTML injection sink with dynamic content",
@@ -6929,6 +7476,7 @@ const dangerousHtmlSink = defineRule({
6929
7476
  const valueTail = fullValueTail.slice(0, VALUE_EXPRESSION_MAX_CHARS);
6930
7477
  const terminatorIndex = valueTail.search(/[;}]/);
6931
7478
  const valueExpression = terminatorIndex >= 0 ? valueTail.slice(0, terminatorIndex + 1) : valueTail;
7479
+ const sinkIndex = lines.slice(0, lineIndex).join("\n").length + (lineIndex > 0 ? 1 : 0) + line.search(DANGEROUS_HTML_PATTERN);
6932
7480
  if (STRING_LITERAL_VALUE_PATTERN.test(valueExpression)) continue;
6933
7481
  if (MODULE_CONSTANT_VALUE_PATTERN.test(valueExpression)) continue;
6934
7482
  if (DOM_CONTENT_SOURCE_VALUE_PATTERN.test(valueExpression) && !valueExpression.includes("+")) {
@@ -6939,8 +7487,8 @@ const dangerousHtmlSink = defineRule({
6939
7487
  const longValueTail = HTML_VALUE_START_PATTERN.exec(lines.slice(lineIndex, lineIndex + 1 + STATIC_TEMPLATE_LOOKAHEAD_LINES).join("\n"))?.[1]?.trimStart();
6940
7488
  let templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
6941
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;
6942
- if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)) {
6943
- const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, file.content);
7490
+ if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression) && findContainingFunctionParameterSource(valueIdentifier, sinkIndex, file.content) === null) {
7491
+ const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, sinkIndex, file.content);
6944
7492
  if (declarationInitializer !== null) {
6945
7493
  if (isPureStringLiteralConcat(declarationInitializer)) continue;
6946
7494
  templateInterpolations = getTemplateInterpolations(declarationInitializer);
@@ -6948,17 +7496,23 @@ const dangerousHtmlSink = defineRule({
6948
7496
  }
6949
7497
  if (templateInterpolations === "") continue;
6950
7498
  const judgedExpression = templateInterpolations ?? valueExpression;
6951
- if (SANITIZER_PATTERN.test(judgedExpression)) continue;
6952
- if (ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
6953
- if (I18N_VALUE_PATTERN.test(judgedExpression)) continue;
6954
- if (!HTML_TAINT_PATTERN.test(judgedExpression)) 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;
6955
7504
  if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
6956
- if (/highlighted/i.test(valueExpression)) continue;
6957
- if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
7505
+ if (isTrustedHighlighterValue(valueExpression, file.content, sinkIndex)) continue;
6958
7506
  if (valueIdentifier !== void 0) {
6959
7507
  const escapedIdentifier = escapeRegExp(valueIdentifier);
6960
- if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
6961
- 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
+ }
6962
7516
  if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
6963
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;
6964
7518
  }
@@ -7043,7 +7597,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
7043
7597
  const getOpeningElementTagName = (openingElement) => {
7044
7598
  if (!openingElement) return null;
7045
7599
  if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
7046
- if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement.name.name;
7600
+ if (isNodeOfType(openingElement.name, "JSXIdentifier")) return resolveJsxElementType(openingElement);
7047
7601
  if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
7048
7602
  let cursor = openingElement.name;
7049
7603
  while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
@@ -7295,7 +7849,7 @@ const dialogHasAccessibleName = defineRule({
7295
7849
  if (isTestlikeFilename(context.filename)) return {};
7296
7850
  return { JSXOpeningElement(node) {
7297
7851
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
7298
- const tagName = node.name.name;
7852
+ const tagName = resolveJsxElementType(node);
7299
7853
  if (tagName[0] !== tagName[0]?.toLowerCase()) return;
7300
7854
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
7301
7855
  const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
@@ -7398,7 +7952,7 @@ const containsJsx$1 = (root) => {
7398
7952
  containsJsxCache.set(root, found);
7399
7953
  return found;
7400
7954
  };
7401
- const getStaticMemberName$1 = (node) => {
7955
+ const getStaticMemberName$2 = (node) => {
7402
7956
  if (!isNodeOfType(node, "MemberExpression")) return null;
7403
7957
  if (!node.computed && isNodeOfType(node.property, "Identifier")) return node.property.name;
7404
7958
  if (node.computed && isNodeOfType(node.property, "Literal") && typeof node.property.value === "string") return node.property.value;
@@ -7412,7 +7966,7 @@ const getAssignedName = (node) => {
7412
7966
  if (isNodeOfType(parent, "AssignmentExpression")) {
7413
7967
  const left = parent.left;
7414
7968
  if (isNodeOfType(left, "Identifier")) return left.name;
7415
- if (isNodeOfType(left, "MemberExpression")) return getStaticMemberName$1(left);
7969
+ if (isNodeOfType(left, "MemberExpression")) return getStaticMemberName$2(left);
7416
7970
  }
7417
7971
  return null;
7418
7972
  };
@@ -7420,20 +7974,20 @@ const isModuleExportsAssignment = (node) => {
7420
7974
  const parent = node.parent;
7421
7975
  if (!parent || !isNodeOfType(parent, "AssignmentExpression")) return false;
7422
7976
  const left = parent.left;
7423
- return isNodeOfType(left, "MemberExpression") && isNodeOfType(left.object, "Identifier") && left.object.name === "module" && getStaticMemberName$1(left) === "exports";
7977
+ return isNodeOfType(left, "MemberExpression") && isNodeOfType(left.object, "Identifier") && left.object.name === "module" && getStaticMemberName$2(left) === "exports";
7424
7978
  };
7425
7979
  const isCreateClassLikeCall = (node) => {
7426
7980
  if (!isNodeOfType(node, "CallExpression")) return false;
7427
7981
  if (isEs5Component(node)) return true;
7428
7982
  const callee = node.callee;
7429
- if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName$1(callee) === "createClass";
7983
+ if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName$2(callee) === "createClass";
7430
7984
  return false;
7431
7985
  };
7432
- const isCreateContextCall = (node) => {
7986
+ const isCreateContextCall$1 = (node) => {
7433
7987
  if (!isNodeOfType(node, "CallExpression")) return false;
7434
7988
  const callee = node.callee;
7435
7989
  if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
7436
- return isNodeOfType(callee, "MemberExpression") && getStaticMemberName$1(callee) === "createContext";
7990
+ return isNodeOfType(callee, "MemberExpression") && getStaticMemberName$2(callee) === "createContext";
7437
7991
  };
7438
7992
  const isNamedFunctionLike = (node) => (isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) && Boolean(node.id?.name);
7439
7993
  const firstCallArgument = (node) => {
@@ -7491,7 +8045,7 @@ const memberExpressionPath = (node) => {
7491
8045
  if (isNodeOfType(node, "Identifier")) return [node.name];
7492
8046
  if (!isNodeOfType(node, "MemberExpression")) return [];
7493
8047
  const objectPath = memberExpressionPath(node.object);
7494
- const propertyName = getStaticMemberName$1(node);
8048
+ const propertyName = getStaticMemberName$2(node);
7495
8049
  return propertyName ? [...objectPath, propertyName] : objectPath;
7496
8050
  };
7497
8051
  const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
@@ -7501,7 +8055,7 @@ const getDisplayNameAssignmentIndex = (programRoot) => {
7501
8055
  const identifierTargets = /* @__PURE__ */ new Set();
7502
8056
  const memberPathSegments = /* @__PURE__ */ new Set();
7503
8057
  const visit = (node) => {
7504
- if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName$1(node.left) === "displayName") {
8058
+ if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName$2(node.left) === "displayName") {
7505
8059
  if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
7506
8060
  for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
7507
8061
  }
@@ -7606,7 +8160,7 @@ const displayName = defineRule({
7606
8160
  }
7607
8161
  },
7608
8162
  CallExpression(node) {
7609
- if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall(node)) {
8163
+ if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall$1(node)) {
7610
8164
  const assignedName = getAssignedName(node);
7611
8165
  if (assignedName) {
7612
8166
  const programRoot = findProgramRoot(node);
@@ -7653,21 +8207,6 @@ const displayName = defineRule({
7653
8207
  }
7654
8208
  });
7655
8209
  //#endregion
7656
- //#region src/plugin/utils/get-static-property-key-name.ts
7657
- const getStaticPropertyKeyName = (node, options = {}) => {
7658
- if (!isNodeOfType(node, "Property")) return null;
7659
- if (node.computed) {
7660
- if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
7661
- return null;
7662
- }
7663
- if (isNodeOfType(node.key, "Identifier")) return node.key.name;
7664
- if (isNodeOfType(node.key, "Literal")) {
7665
- if (typeof node.key.value === "string") return node.key.value;
7666
- if (options.stringifyNonStringLiterals) return String(node.key.value);
7667
- }
7668
- return null;
7669
- };
7670
- //#endregion
7671
8210
  //#region src/plugin/utils/read-static-boolean.ts
7672
8211
  const readStaticBoolean = (node) => {
7673
8212
  if (!node) return null;
@@ -7676,31 +8215,16 @@ const readStaticBoolean = (node) => {
7676
8215
  return unwrappedNode.value;
7677
8216
  };
7678
8217
  //#endregion
7679
- //#region src/plugin/utils/resolve-const-identifier-alias.ts
7680
- const resolveConstIdentifierAlias = (identifier, scopes) => {
7681
- if (!isNodeOfType(identifier, "Identifier")) return null;
7682
- const visitedSymbolIds = /* @__PURE__ */ new Set();
7683
- let symbol = scopes.symbolFor(identifier);
7684
- while (symbol?.kind === "const") {
7685
- if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
7686
- visitedSymbolIds.add(symbol.id);
7687
- const initializer = stripParenExpression(symbol.initializer);
7688
- if (!isNodeOfType(initializer, "Identifier")) return symbol;
7689
- symbol = scopes.symbolFor(initializer);
7690
- }
7691
- return symbol;
7692
- };
7693
- //#endregion
7694
8218
  //#region src/plugin/utils/is-react-api-call.ts
7695
8219
  const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
7696
8220
  const isImportedFromReact = (symbol) => {
7697
8221
  if (symbol.kind !== "import") return false;
7698
8222
  const importDeclaration = symbol.declarationNode.parent;
7699
- return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react");
8223
+ return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
7700
8224
  };
7701
- const isNamedReactApiImport = (identifier, apiNames, scopes) => {
8225
+ const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
7702
8226
  if (!isNodeOfType(identifier, "Identifier")) return false;
7703
- const symbol = scopes.symbolFor(identifier);
8227
+ const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
7704
8228
  if (!symbol || !isImportedFromReact(symbol)) return false;
7705
8229
  const importedName = getImportedName(symbol.declarationNode);
7706
8230
  return Boolean(importedName && includesApiName(apiNames, importedName));
@@ -7714,7 +8238,7 @@ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
7714
8238
  if (!isNodeOfType(node, "CallExpression")) return false;
7715
8239
  const callee = stripParenExpression(node.callee);
7716
8240
  if (isNodeOfType(callee, "Identifier")) {
7717
- if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
8241
+ if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
7718
8242
  return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
7719
8243
  }
7720
8244
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
@@ -8817,6 +9341,30 @@ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, conte
8817
9341
  }
8818
9342
  return matchingBlocks.size > 0;
8819
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
+ };
8820
9368
  const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
8821
9369
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8822
9370
  if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
@@ -9017,6 +9565,83 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
9017
9565
  });
9018
9566
  return didCleanupFunctionMatch;
9019
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);
9020
9645
  const effectHasCleanupForUsage = (callback, usage, context) => {
9021
9646
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
9022
9647
  if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
@@ -9029,6 +9654,10 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
9029
9654
  if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
9030
9655
  const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
9031
9656
  if (!returnedValue) return;
9657
+ if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) {
9658
+ matchingCleanupReturns.push(child);
9659
+ return;
9660
+ }
9032
9661
  if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
9033
9662
  matchingCleanupReturns.push(child);
9034
9663
  return;
@@ -9044,13 +9673,17 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
9044
9673
  if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
9045
9674
  }
9046
9675
  const cleanupFunction = resolveStableValue(returnedValue, context);
9676
+ if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) {
9677
+ matchingCleanupReturns.push(child);
9678
+ return;
9679
+ }
9047
9680
  if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
9048
9681
  if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
9049
9682
  });
9050
9683
  return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
9051
9684
  };
9052
9685
  const findFirstUsageWithoutCleanup = (callback, usages, context) => {
9053
- 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;
9054
9687
  return null;
9055
9688
  };
9056
9689
  const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
@@ -9646,6 +10279,11 @@ const walkParameterReferences = (pattern, state) => {
9646
10279
  const walk = (node, state) => {
9647
10280
  if (isFunctionLike$1(node)) {
9648
10281
  if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
10282
+ const functionParams = node.params ?? [];
10283
+ for (const param of functionParams) {
10284
+ if (!("decorators" in param) || !Array.isArray(param.decorators)) continue;
10285
+ for (const decorator of param.decorators) if (isAstNode(decorator)) walk(decorator, state);
10286
+ }
9649
10287
  setNodeScope(node, state);
9650
10288
  const fnScope = pushScope(node.type === "ArrowFunctionExpression" ? "arrow-function" : "function", node, state);
9651
10289
  if ((isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) && node.id) {
@@ -9658,7 +10296,6 @@ const walk = (node, state) => {
9658
10296
  });
9659
10297
  tagAsBinding(state, node.id);
9660
10298
  }
9661
- const functionParams = node.params ?? [];
9662
10299
  handleFunctionParameters(functionParams, fnScope, state);
9663
10300
  for (const param of functionParams) walkParameterReferences(param, state);
9664
10301
  const body = node.body;
@@ -9668,6 +10305,9 @@ const walk = (node, state) => {
9668
10305
  }
9669
10306
  if (isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression")) {
9670
10307
  if (isNodeOfType(node, "ClassDeclaration") && node.id) handleClassDeclaration(node, state);
10308
+ if (Array.isArray(node.decorators)) {
10309
+ for (const decorator of node.decorators) if (isAstNode(decorator)) walk(decorator, state);
10310
+ }
9671
10311
  const classScope = pushScope("class", node, state);
9672
10312
  setNodeScope(node, state);
9673
10313
  if (isNodeOfType(node, "ClassExpression") && node.id) {
@@ -9870,7 +10510,7 @@ const computeClosureCaptures = (functionNode, scopes) => {
9870
10510
  const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
9871
10511
  const out = [];
9872
10512
  const seen = /* @__PURE__ */ new Set();
9873
- const visit = (node) => {
10513
+ walkAst(functionNode, (node) => {
9874
10514
  if (node !== functionNode && isFunctionLike$1(node)) {
9875
10515
  const innerCaptures = closureCaptures(node, scopes);
9876
10516
  for (const reference of innerCaptures) if (reference.resolvedSymbol && !isDescendantScope(reference.resolvedSymbol.scope, functionScope)) {
@@ -9879,7 +10519,7 @@ const computeClosureCaptures = (functionNode, scopes) => {
9879
10519
  seen.add(reference.id);
9880
10520
  }
9881
10521
  }
9882
- return;
10522
+ return false;
9883
10523
  }
9884
10524
  const reference = scopes.referenceFor(node);
9885
10525
  if (reference && reference.resolvedSymbol) {
@@ -9890,17 +10530,7 @@ const computeClosureCaptures = (functionNode, scopes) => {
9890
10530
  }
9891
10531
  }
9892
10532
  }
9893
- const record = node;
9894
- for (const key of Object.keys(record)) {
9895
- if (key === "parent") continue;
9896
- if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
9897
- const child = record[key];
9898
- if (Array.isArray(child)) {
9899
- for (const item of child) if (isAstNode(item)) visit(item);
9900
- } else if (isAstNode(child)) visit(child);
9901
- }
9902
- };
9903
- visit(functionNode);
10533
+ });
9904
10534
  return out;
9905
10535
  };
9906
10536
  const capturesByAnalysis = /* @__PURE__ */ new WeakMap();
@@ -10926,6 +11556,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
10926
11556
  return { CallExpression(node) {
10927
11557
  const hookName = getHookName(node.callee, context.scopes);
10928
11558
  if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
11559
+ if (HOOKS_REQUIRING_DEPS_MATCH.has(hookName) && !isReactApiCall(node, HOOKS_REQUIRING_DEPS_MATCH, context.scopes, {
11560
+ allowGlobalReactNamespace: true,
11561
+ allowUnboundBareCalls: true,
11562
+ resolveNamedAliases: true
11563
+ })) return;
10929
11564
  const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
10930
11565
  const depsArgumentIndex = getDepsArgumentIndex(hookName);
10931
11566
  const callbackArgument = node.arguments[callbackArgumentIndex];
@@ -11446,7 +12081,7 @@ const forbidDomProps = defineRule({
11446
12081
  return { JSXOpeningElement(node) {
11447
12082
  if (forbidMap.size === 0) return;
11448
12083
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
11449
- const elementName = node.name.name;
12084
+ const elementName = resolveJsxElementType(node);
11450
12085
  if (isReactComponentName(elementName)) return;
11451
12086
  for (const attribute of node.attributes) {
11452
12087
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -11524,7 +12159,7 @@ const forbidElements = defineRule({
11524
12159
  return {
11525
12160
  JSXOpeningElement(node) {
11526
12161
  if (forbidMap.size === 0) return;
11527
- const fullName = flattenJsxName$1(node.name);
12162
+ const fullName = resolveJsxElementType(node);
11528
12163
  if (!fullName || !forbidMap.has(fullName)) return;
11529
12164
  context.report({
11530
12165
  node: node.name,
@@ -11886,8 +12521,7 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
11886
12521
  const isParagraphElement = (candidate) => {
11887
12522
  if (!isNodeOfType(candidate, "JSXElement")) return false;
11888
12523
  const opening = candidate.openingElement;
11889
- if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
11890
- return opening.name.name === "p";
12524
+ return resolveJsxElementType(opening) === "p";
11891
12525
  };
11892
12526
  const findEnclosingParagraph = (openingElement) => {
11893
12527
  const owningElement = openingElement.parent;
@@ -11908,8 +12542,7 @@ const htmlNoInvalidParagraphChild = defineRule({
11908
12542
  severity: "warn",
11909
12543
  recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
11910
12544
  create: (context) => ({ JSXOpeningElement(node) {
11911
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
11912
- const childTagName = node.name.name;
12545
+ const childTagName = resolveJsxElementType(node);
11913
12546
  if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
11914
12547
  if (!findEnclosingParagraph(node)) return;
11915
12548
  context.report({
@@ -11940,7 +12573,7 @@ const getHostTagName = (jsxElement) => {
11940
12573
  if (!isNodeOfType(jsxElement, "JSXElement")) return null;
11941
12574
  const opening = jsxElement.openingElement;
11942
12575
  if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
11943
- const tagName = opening.name.name;
12576
+ const tagName = resolveJsxElementType(opening);
11944
12577
  if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
11945
12578
  return tagName;
11946
12579
  };
@@ -11970,7 +12603,7 @@ const findClosestHostAncestor = (jsxElement) => {
11970
12603
  if (isNodeOfType(ancestor, "JSXElement")) {
11971
12604
  const opening = ancestor.openingElement;
11972
12605
  if (isNodeOfType(opening.name, "JSXIdentifier")) {
11973
- const ancestorTag = opening.name.name;
12606
+ const ancestorTag = resolveJsxElementType(opening);
11974
12607
  if (ancestorTag.length === 0) {
11975
12608
  previous = ancestor;
11976
12609
  ancestor = ancestor.parent ?? null;
@@ -12052,8 +12685,7 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
12052
12685
  const isJsxElementWithTagName = (candidate, tagName) => {
12053
12686
  if (!isNodeOfType(candidate, "JSXElement")) return false;
12054
12687
  const opening = candidate.openingElement;
12055
- if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
12056
- return opening.name.name === tagName;
12688
+ return resolveJsxElementType(opening) === tagName;
12057
12689
  };
12058
12690
  const findEnclosingSameTag = (openingElement, tagName) => {
12059
12691
  const owningElement = openingElement.parent;
@@ -12074,8 +12706,7 @@ const htmlNoNestedInteractive = defineRule({
12074
12706
  severity: "warn",
12075
12707
  recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
12076
12708
  create: (context) => ({ JSXOpeningElement(node) {
12077
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
12078
- const tagName = node.name.name;
12709
+ const tagName = resolveJsxElementType(node);
12079
12710
  if (tagName !== "a" && tagName !== "button") return;
12080
12711
  if (!findEnclosingSameTag(node, tagName)) return;
12081
12712
  context.report({
@@ -12190,7 +12821,7 @@ const iframeMissingSandbox = defineRule({
12190
12821
  matchByOccurrence: true,
12191
12822
  create: skipNonProductionFiles((context) => ({
12192
12823
  JSXOpeningElement(node) {
12193
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "iframe") return;
12824
+ if (resolveJsxElementType(node) !== "iframe") return;
12194
12825
  const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
12195
12826
  if (!sandboxAttr) {
12196
12827
  const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
@@ -12426,30 +13057,6 @@ const insecureCryptoRisk = defineRule({
12426
13057
  }
12427
13058
  });
12428
13059
  //#endregion
12429
- //#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
12430
- const findMatchingBracket = (content, openIndex) => {
12431
- const open = content[openIndex];
12432
- const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
12433
- if (close === "") return -1;
12434
- let depth = 0;
12435
- let stringDelimiter = null;
12436
- for (let index = openIndex; index < content.length; index += 1) {
12437
- const character = content[index];
12438
- if (stringDelimiter !== null) {
12439
- if (character === "\\") index += 1;
12440
- else if (character === stringDelimiter) stringDelimiter = null;
12441
- continue;
12442
- }
12443
- if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
12444
- else if (character === open) depth += 1;
12445
- else if (character === close) {
12446
- depth -= 1;
12447
- if (depth === 0) return index;
12448
- }
12449
- }
12450
- return -1;
12451
- };
12452
- //#endregion
12453
13060
  //#region src/plugin/rules/security-scan/insecure-session-cookie.ts
12454
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])`;
12455
13062
  const AUTH_COOKIE_NAME_LITERAL = `[\`"'][^\`"']*?${AUTH_COOKIE_NAME_TOKEN}[^\`"']*[\`"']`;
@@ -13206,6 +13813,28 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
13206
13813
  } })
13207
13814
  });
13208
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
13209
13838
  //#region src/plugin/rules/js-performance/js-batch-dom-css.ts
13210
13839
  const ITERATOR_METHOD_NAMES$2 = new Set([
13211
13840
  "forEach",
@@ -13390,9 +14019,19 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
13390
14019
  });
13391
14020
  return foundAttachment;
13392
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
+ };
13393
14031
  const isProvablyDetachedAtWrite = (styleWriteStatement) => {
13394
- if (!isNodeOfType(styleWriteStatement, "ExpressionStatement") || !isNodeOfType(styleWriteStatement.expression, "AssignmentExpression") || !isNodeOfType(styleWriteStatement.expression.left, "MemberExpression") || !isNodeOfType(styleWriteStatement.expression.left.object, "MemberExpression")) return false;
13395
- 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;
13396
14035
  const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
13397
14036
  if (!creationRoot) return false;
13398
14037
  return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
@@ -13404,15 +14043,17 @@ const jsBatchDomCss = defineRule({
13404
14043
  severity: "warn",
13405
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",
13406
14045
  create: (context) => {
13407
- 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";
13408
- 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
+ };
13409
14050
  return { BlockStatement(node) {
13410
14051
  const perIterationBody = findEnclosingPerIterationBody(node);
13411
14052
  if (!perIterationBody) return;
13412
14053
  const statements = node.body ?? [];
13413
14054
  let layoutReads = null;
13414
14055
  for (let statementIndex = 1; statementIndex < statements.length; statementIndex++) {
13415
- if (!isStyleAssignment(statements[statementIndex]) || !isStyleAssignment(statements[statementIndex - 1])) continue;
14056
+ if (getStyleAssignment(statements[statementIndex]) === null || getStyleAssignment(statements[statementIndex - 1]) === null) continue;
13416
14057
  if (!writesLayoutAffectingProperty(statements[statementIndex]) && !writesLayoutAffectingProperty(statements[statementIndex - 1])) continue;
13417
14058
  layoutReads ??= scanPerIterationLayoutReads(perIterationBody);
13418
14059
  if (!layoutReads.hasUsedLayoutRead || layoutReads.hasDeliberateForcedReflow) return;
@@ -14108,6 +14749,18 @@ const jsHoistRegexp = defineRule({
14108
14749
  }, { treatIteratorCallbacksAsLoops: true })
14109
14750
  });
14110
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
14111
14764
  //#region src/plugin/rules/js-performance/js-index-maps.ts
14112
14765
  const referencesParameter = (expression, parameterName) => {
14113
14766
  if (!expression) return false;
@@ -14118,7 +14771,7 @@ const referencesParameter = (expression, parameterName) => {
14118
14771
  const isSingleFieldEqualityPredicate = (node) => {
14119
14772
  const callback = node.arguments?.[0];
14120
14773
  if (!isInlineFunctionExpression(callback)) return false;
14121
- const firstParameter = callback.params?.[0];
14774
+ const firstParameter = resolveFirstArgumentBinding(callback.params?.[0]);
14122
14775
  if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
14123
14776
  let predicate = null;
14124
14777
  const body = callback.body;
@@ -14220,7 +14873,11 @@ const jsIndexMaps = defineRule({
14220
14873
  //#region src/plugin/utils/are-expressions-structurally-equal.ts
14221
14874
  const areExpressionsStructurallyEqual = (a, b) => {
14222
14875
  if (!a || !b) return a === b;
14876
+ const unwrappedA = stripParenExpression(a);
14877
+ const unwrappedB = stripParenExpression(b);
14878
+ if (unwrappedA !== a || unwrappedB !== b) return areExpressionsStructurallyEqual(unwrappedA, unwrappedB);
14223
14879
  if (a.type !== b.type) return false;
14880
+ if (isNodeOfType(a, "ThisExpression")) return true;
14224
14881
  if (isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier")) return a.name === b.name;
14225
14882
  if (isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal")) return a.value === b.value;
14226
14883
  if (isNodeOfType(a, "MemberExpression") && isNodeOfType(b, "MemberExpression")) {
@@ -14806,9 +15463,21 @@ const isSmallFixedListMember = (receiver) => {
14806
15463
  };
14807
15464
  const getResolvedInitializer = (receiver) => {
14808
15465
  if (!isNodeOfType(receiver, "Identifier")) return null;
14809
- const initializer = findVariableInitializer(receiver, receiver.name)?.initializer ?? null;
14810
- if (initializer && isNodeOfType(initializer, "Identifier")) return findVariableInitializer(initializer, initializer.name)?.initializer ?? initializer;
14811
- 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
+ };
14812
15481
  };
14813
15482
  const isSplitCall = (expression) => {
14814
15483
  if (!expression) return false;
@@ -14974,8 +15643,8 @@ const isBoundedConstantCollection = (collection) => {
14974
15643
  if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
14975
15644
  if (isSmallInlineLiteralArray(stripped)) return true;
14976
15645
  if (isNodeOfType(stripped, "Identifier")) {
14977
- const initializer = getResolvedInitializer(stripped);
14978
- if (initializer && isSmallInlineLiteralArray(initializer)) return true;
15646
+ const resolved = getResolvedInitializer(stripped);
15647
+ if (resolved && !resolved.isDefault && isSmallInlineLiteralArray(resolved.initializer)) return true;
14979
15648
  }
14980
15649
  return false;
14981
15650
  };
@@ -15027,8 +15696,8 @@ const jsSetMapLookups = defineRule({
15027
15696
  if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
15028
15697
  const resolvedInitializer = getResolvedInitializer(receiver);
15029
15698
  if (resolvedInitializer) {
15030
- if (isLikelyStringReceiver(resolvedInitializer)) return;
15031
- if (isSmallInlineLiteralArray(resolvedInitializer)) return;
15699
+ if (isLikelyStringReceiver(resolvedInitializer.initializer)) return;
15700
+ if (!resolvedInitializer.isDefault && isSmallInlineLiteralArray(resolvedInitializer.initializer)) return;
15032
15701
  }
15033
15702
  if (isStringElementOfSplitIteration(receiver)) return;
15034
15703
  if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
@@ -15441,14 +16110,21 @@ const jsxFilenameExtension = defineRule({
15441
16110
  });
15442
16111
  //#endregion
15443
16112
  //#region src/plugin/utils/is-jsx-fragment-element.ts
15444
- const isJsxFragmentElement = (node) => {
16113
+ const isJsxFragmentElement = (node, scopes) => {
15445
16114
  if (!isNodeOfType(node, "JSXOpeningElement")) return false;
15446
16115
  const elementName = node.name;
15447
- 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
+ }
15448
16122
  if (isNodeOfType(elementName, "JSXMemberExpression")) {
15449
16123
  if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
15450
- if (elementName.object.name !== "React") return false;
15451
- 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);
15452
16128
  }
15453
16129
  return false;
15454
16130
  };
@@ -15474,7 +16150,7 @@ const jsxFragments = defineRule({
15474
16150
  if (mode !== "syntax") return;
15475
16151
  if (!node.closingElement) return;
15476
16152
  const openingElement = node.openingElement;
15477
- if (!isJsxFragmentElement(openingElement)) return;
16153
+ if (!isJsxFragmentElement(openingElement, context.scopes)) return;
15478
16154
  if (openingElement.attributes.length > 0) return;
15479
16155
  context.report({
15480
16156
  node: openingElement,
@@ -18155,10 +18831,6 @@ const resolveSettings$29 = (settings) => {
18155
18831
  if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
18156
18832
  return reactDoctor.jsxNoScriptUrl ?? {};
18157
18833
  };
18158
- const getElementName = (node) => {
18159
- if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
18160
- return null;
18161
- };
18162
18834
  const isLinkPropForElement = (elementName, attributeName, options) => {
18163
18835
  if (elementName === "a" && attributeName === "href") return true;
18164
18836
  const explicit = options.components?.[elementName];
@@ -18178,7 +18850,8 @@ const jsxNoScriptUrl = defineRule({
18178
18850
  create: (context) => {
18179
18851
  const options = resolveSettings$29(context.settings);
18180
18852
  return { JSXOpeningElement(node) {
18181
- const elementName = getElementName(node);
18853
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
18854
+ const elementName = resolveJsxElementType(node);
18182
18855
  if (!elementName) return;
18183
18856
  for (const attribute of node.attributes) {
18184
18857
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -18365,11 +19038,6 @@ const checkTarget = (attributeValue) => {
18365
19038
  alternate: false
18366
19039
  };
18367
19040
  };
18368
- const getOpeningElementName = (node) => {
18369
- const name = node.name;
18370
- if (isNodeOfType(name, "JSXIdentifier")) return name.name;
18371
- return null;
18372
- };
18373
19041
  const jsxNoTargetBlank = defineRule({
18374
19042
  id: "jsx-no-target-blank",
18375
19043
  title: "Unsafe target=_blank link",
@@ -18389,7 +19057,8 @@ const jsxNoTargetBlank = defineRule({
18389
19057
  return settings.formComponents.has(tagName);
18390
19058
  };
18391
19059
  return { JSXOpeningElement(node) {
18392
- const tagName = getOpeningElementName(node);
19060
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
19061
+ const tagName = resolveJsxElementType(node);
18393
19062
  if (!tagName) return;
18394
19063
  if (!isLink(tagName) && !isForm(tagName)) return;
18395
19064
  const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
@@ -18622,7 +19291,7 @@ const jsxNoUselessFragment = defineRule({
18622
19291
  return {
18623
19292
  JSXElement(node) {
18624
19293
  const openingElement = node.openingElement;
18625
- if (!isJsxFragmentElement(openingElement)) return;
19294
+ if (!isJsxFragmentElement(openingElement, context.scopes)) return;
18626
19295
  if (hasJsxKeyAttribute(openingElement)) return;
18627
19296
  if (checkChildren(node, openingElement, node.children)) return;
18628
19297
  if (isChildOfHtmlElement(node)) context.report({
@@ -18924,6 +19593,7 @@ const jwtInsecureVerification = defineRule({
18924
19593
  });
18925
19594
  //#endregion
18926
19595
  //#region src/plugin/rules/security-scan/key-lifecycle-risk.ts
19596
+ const KEY_MATERIAL_MARKER_PATTERN = /PRIVATE KEY|SSH_PRIVATE_KEY|GPG_PRIVATE_KEY|DEPLOY_KEY|SIGNING_KEY/i;
18927
19597
  const keyLifecycleRisk = defineRule({
18928
19598
  id: "key-lifecycle-risk",
18929
19599
  title: "Long-lived key material in repository",
@@ -18931,7 +19601,7 @@ const keyLifecycleRisk = defineRule({
18931
19601
  committedFilesOnly: true,
18932
19602
  recommendation: "Remove private keys from source, rotate exposed credentials, prefer short-lived deploy credentials, and document revocation/expiry for release keys.",
18933
19603
  scan: scanByPattern({
18934
- shouldScan: (file) => !TEST_CONTEXT_PATTERN.test(file.relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath),
19604
+ shouldScan: (file) => !TEST_CONTEXT_PATTERN.test(file.relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath) && KEY_MATERIAL_MARKER_PATTERN.test(file.content),
18935
19605
  pattern: /(?<!(?:placeholder|example|sample|dummy|fake)[\s\S]{0,40})-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----(?:\s|\\r|\\n)*[A-Za-z0-9+/=][A-Za-z0-9+/=\s]{38,}(?![^-]{0,160}\.\.\.)|\b(?:SSH_PRIVATE_KEY|GPG_PRIVATE_KEY|DEPLOY_KEY|SIGNING_KEY)\b\s*[:=]\s*["'][^"'\n]{16,}["']/i,
18936
19606
  message: "Private or long-lived release key material appears in the repository."
18937
19607
  })
@@ -19637,15 +20307,21 @@ const localRpcNativeBridgeRisk = defineRule({
19637
20307
  message: "Code appears to bridge browser code to localhost/native capabilities with weak origin or update/install checks."
19638
20308
  })
19639
20309
  });
20310
+ //#endregion
20311
+ //#region src/plugin/rules/security-scan/mcp-tool-capability-risk.ts
20312
+ const MCP_IMPORT_PATTERN = /\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/;
20313
+ const MCP_IMPORT_PREFILTER_PATTERN = /@modelcontextprotocol\/sdk|\bMcpServer\b|\bMcpAgent\b/;
20314
+ const MCP_TOOL_SURFACE_PATTERN = /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/;
20315
+ const MCP_TOOL_SURFACE_PREFILTER_PATTERN = /\b(?:tool|registerTool|setRequestHandler)\b/;
19640
20316
  const mcpToolCapabilityRisk = defineRule({
19641
20317
  id: "mcp-tool-capability-risk",
19642
20318
  title: "MCP tool exposes dangerous capability",
19643
20319
  severity: "warn",
19644
20320
  recommendation: "MCP tool calls run with the connecting client's authority. Validate inputs, enforce per-tool authorization, and avoid raw filesystem/shell/network access where possible.",
19645
20321
  scan: scanByPattern({
19646
- shouldScan: (file) => isProductionSourcePath(file.relativePath),
19647
- pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
19648
- requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
20322
+ shouldScan: (file) => isProductionSourcePath(file.relativePath) && MCP_IMPORT_PREFILTER_PATTERN.test(file.content) && MCP_TOOL_SURFACE_PREFILTER_PATTERN.test(file.content) && AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN.test(file.content),
20323
+ pattern: MCP_TOOL_SURFACE_PATTERN,
20324
+ requireAll: [MCP_IMPORT_PATTERN, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
19649
20325
  ignoreStringLiterals: true,
19650
20326
  message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
19651
20327
  })
@@ -19857,11 +20533,24 @@ const hasDirective = (programNode, directive) => {
19857
20533
  return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
19858
20534
  };
19859
20535
  //#endregion
19860
- //#region src/plugin/utils/is-uppercase-name.ts
19861
- const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
19862
- //#endregion
19863
- //#region src/plugin/utils/is-component-assignment.ts
19864
- 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
+ };
19865
20554
  //#endregion
19866
20555
  //#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
19867
20556
  const nextjsAsyncClientComponent = defineRule({
@@ -19887,10 +20576,10 @@ const nextjsAsyncClientComponent = defineRule({
19887
20576
  },
19888
20577
  VariableDeclarator(node) {
19889
20578
  if (!fileHasUseClient) return;
19890
- if (!isComponentAssignment(node)) return;
19891
- if (!isInlineFunctionExpression(node.init)) return;
19892
- if (!node.init.async) return;
19893
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;
19894
20583
  context.report({
19895
20584
  node,
19896
20585
  message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
@@ -20192,7 +20881,7 @@ const nextjsNoAElement = defineRule({
20192
20881
  severity: "warn",
20193
20882
  recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
20194
20883
  create: (context) => ({ JSXOpeningElement(node) {
20195
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
20884
+ if (resolveJsxElementType(node) !== "a") return;
20196
20885
  const attributes = node.attributes ?? [];
20197
20886
  const downloadAttribute = findJsxAttribute(attributes, "download");
20198
20887
  if (downloadAttribute) {
@@ -20388,7 +21077,7 @@ const nextjsNoCssLink = defineRule({
20388
21077
  severity: "warn",
20389
21078
  recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
20390
21079
  create: (context) => ({ JSXOpeningElement(node) {
20391
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
21080
+ if (resolveJsxElementType(node) !== "link") return;
20392
21081
  const attributes = node.attributes ?? [];
20393
21082
  const relAttribute = findJsxAttribute(attributes, "rel");
20394
21083
  if (!relAttribute?.value) return;
@@ -20496,7 +21185,7 @@ const nextjsNoFontLink = defineRule({
20496
21185
  severity: "warn",
20497
21186
  recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
20498
21187
  create: (context) => ({ JSXOpeningElement(node) {
20499
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
21188
+ if (resolveJsxElementType(node) !== "link") return;
20500
21189
  const attributes = node.attributes ?? [];
20501
21190
  const hrefAttribute = findJsxAttribute(attributes, "href");
20502
21191
  if (!hrefAttribute?.value) return;
@@ -20671,7 +21360,7 @@ const nextjsNoImgElement = defineRule({
20671
21360
  create: (context) => {
20672
21361
  if (isGeneratedImageRenderContext(context)) return {};
20673
21362
  return { JSXOpeningElement(node) {
20674
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
21363
+ if (resolveJsxElementType(node) !== "img") return;
20675
21364
  if (isGeneratedImageRenderContext(context, node)) return;
20676
21365
  const programRoot = findProgramRoot(node);
20677
21366
  if (programRoot && hasEmailTemplateImport(programRoot)) return;
@@ -20718,8 +21407,8 @@ const nextjsNoPolyfillScript = defineRule({
20718
21407
  severity: "warn",
20719
21408
  recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
20720
21409
  create: (context) => ({ JSXOpeningElement(node) {
20721
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
20722
- if (node.name.name !== "script" && node.name.name !== "Script") return;
21410
+ const elementName = resolveJsxElementType(node);
21411
+ if (elementName !== "script" && elementName !== "Script") return;
20723
21412
  const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
20724
21413
  if (!srcAttribute?.value) return;
20725
21414
  const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
@@ -20756,8 +21445,8 @@ const catchClauseRethrowsCaught = (handler) => {
20756
21445
  return didRethrow;
20757
21446
  };
20758
21447
  //#endregion
20759
- //#region src/plugin/utils/find-guarding-try-statement.ts
20760
- const isImmediatelyInvokedFunctionCallee = (functionNode) => {
21448
+ //#region src/plugin/utils/is-immediately-invoked-function.ts
21449
+ const isImmediatelyInvokedFunction = (functionNode) => {
20761
21450
  let wrappedCallee = functionNode;
20762
21451
  let enclosing = functionNode.parent;
20763
21452
  while (enclosing && stripParenExpression(enclosing) === functionNode) {
@@ -20766,11 +21455,13 @@ const isImmediatelyInvokedFunctionCallee = (functionNode) => {
20766
21455
  }
20767
21456
  return Boolean(enclosing && isNodeOfType(enclosing, "CallExpression") && enclosing.callee === wrappedCallee);
20768
21457
  };
21458
+ //#endregion
21459
+ //#region src/plugin/utils/find-guarding-try-statement.ts
20769
21460
  const findGuardingTryStatement = (node) => {
20770
21461
  let child = node;
20771
21462
  let ancestor = node.parent;
20772
21463
  while (ancestor) {
20773
- if (isFunctionLike$1(ancestor) && !isImmediatelyInvokedFunctionCallee(ancestor)) return null;
21464
+ if (isFunctionLike$1(ancestor) && !isImmediatelyInvokedFunction(ancestor)) return null;
20774
21465
  if (isNodeOfType(ancestor, "TryStatement") && ancestor.block === child && ancestor.handler && !catchClauseRethrowsCaught(ancestor.handler)) return ancestor;
20775
21466
  child = ancestor;
20776
21467
  ancestor = ancestor.parent ?? null;
@@ -21322,7 +22013,7 @@ const FILENAME_TO_LANG = {
21322
22013
  const resolveLang = (filename) => {
21323
22014
  return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
21324
22015
  };
21325
- const parseSourceText = (filename, sourceText) => {
22016
+ const parseSourceText = ({ filename, sourceText, shouldAttachParentReferences = true }) => {
21326
22017
  try {
21327
22018
  const result = parseSync(filename, sourceText, {
21328
22019
  astType: "ts",
@@ -21330,7 +22021,7 @@ const parseSourceText = (filename, sourceText) => {
21330
22021
  });
21331
22022
  if (result.errors.some((parseError) => parseError.severity === "Error")) return null;
21332
22023
  const parsedProgram = result.program;
21333
- attachParentReferences(parsedProgram);
22024
+ if (shouldAttachParentReferences) attachParentReferences(parsedProgram);
21334
22025
  return parsedProgram;
21335
22026
  } catch {
21336
22027
  return null;
@@ -21368,7 +22059,10 @@ const parseSourceFile = (absoluteFilePath) => {
21368
22059
  });
21369
22060
  return null;
21370
22061
  }
21371
- const parsedProgram = parseSourceText(absoluteFilePath, sourceText);
22062
+ const parsedProgram = parseSourceText({
22063
+ filename: absoluteFilePath,
22064
+ sourceText
22065
+ });
21372
22066
  parseCache.set(absoluteFilePath, {
21373
22067
  mtimeMs: fileStat.mtimeMs,
21374
22068
  size: fileStat.size,
@@ -21900,6 +22594,8 @@ const DOM_QUERY_MEMBER_NAMES = new Set([
21900
22594
  ]);
21901
22595
  const LAYOUT_MEASUREMENT_MEMBER_NAMES = new Set([
21902
22596
  "current",
22597
+ "textContent",
22598
+ "innerText",
21903
22599
  "scrollWidth",
21904
22600
  "clientWidth",
21905
22601
  "offsetWidth",
@@ -21921,7 +22617,7 @@ const POST_MOUNT_GLOBAL_NAMES = new Set([
21921
22617
  "navigator"
21922
22618
  ]);
21923
22619
  const REF_FACTORY_CALLEE_NAMES = new Set(["useRef", "createRef"]);
21924
- const hasRefLikeName = (name) => name === "ref" || name.endsWith("Ref") || name.endsWith("ref");
22620
+ const hasRefLikeName = (name) => name === "ref" || name.endsWith("Ref") || name.endsWith("ref") || name.endsWith("Node") || name.endsWith("node") || name.endsWith("Element") || name.endsWith("element");
21925
22621
  const isRefFactoryInitializer = (init) => {
21926
22622
  if (!init || !isNodeOfType(init, "CallExpression")) return false;
21927
22623
  const callee = init.callee;
@@ -22004,88 +22700,37 @@ const readsPostMountValue = (root) => {
22004
22700
  return found;
22005
22701
  };
22006
22702
  //#endregion
22007
- //#region src/plugin/rules/state-and-effects/utils/effect/constants.ts
22008
- const TYPESCRIPT_VISITOR_KEYS = {
22009
- TSAsExpression: ["expression", "typeAnnotation"],
22010
- TSNonNullExpression: ["expression"],
22011
- TSSatisfiesExpression: ["expression", "typeAnnotation"],
22012
- TSTypeAssertion: ["typeAnnotation", "expression"],
22013
- TSInstantiationExpression: ["expression", "typeArguments"]
22014
- };
22015
- const VISITOR_KEYS = {
22016
- ...eslintVisitorKeys.KEYS,
22017
- ...TYPESCRIPT_VISITOR_KEYS
22018
- };
22703
+ //#region src/plugin/rules/state-and-effects/utils/effect/get-ast-child-keys.ts
22704
+ const getAstChildKeys = (node) => visitorKeys[node.type] ?? Object.keys(node).filter((key) => key !== "parent");
22019
22705
  //#endregion
22020
22706
  //#region src/plugin/rules/state-and-effects/utils/effect/get-program-analysis.ts
22021
22707
  const programToAnalysis = /* @__PURE__ */ new WeakMap();
22022
- const stripAndRecordParents = (root) => {
22023
- const restorations = [];
22024
- const seen = /* @__PURE__ */ new WeakSet();
22025
- const visit = (value) => {
22026
- if (!value || typeof value !== "object") return;
22027
- if (seen.has(value)) return;
22028
- seen.add(value);
22029
- if (Array.isArray(value)) {
22030
- for (const item of value) visit(item);
22031
- return;
22032
- }
22033
- const record = value;
22034
- if (!("type" in record)) return;
22035
- if ("parent" in record) {
22036
- restorations.push({
22037
- node: record,
22038
- originalParent: record.parent
22039
- });
22040
- record.parent = null;
22041
- }
22042
- for (const key of Object.keys(record)) {
22043
- if (key === "parent") continue;
22044
- visit(record[key]);
22045
- }
22046
- };
22047
- visit(root);
22048
- return restorations;
22049
- };
22050
- const restoreParents = (restorations) => {
22051
- for (const restoration of restorations) restoration.node.parent = restoration.originalParent;
22052
- };
22053
22708
  const getProgramAnalysis = (anyNode) => {
22054
22709
  const programNode = findProgramRoot(anyNode);
22055
22710
  if (!programNode) return null;
22056
22711
  const cached = programToAnalysis.get(programNode);
22057
22712
  if (cached) return cached;
22058
- const restorations = stripAndRecordParents(programNode);
22059
- let scopeManager;
22060
- try {
22061
- scopeManager = analyze(programNode, {
22062
- ecmaVersion: 2024,
22063
- sourceType: "module",
22064
- childVisitorKeys: VISITOR_KEYS,
22065
- fallback: "iteration"
22066
- });
22067
- } finally {
22068
- restoreParents(restorations);
22069
- }
22070
22713
  const analysis = {
22071
22714
  programNode,
22072
- scopeManager
22715
+ scopeManager: analyze(programNode, {
22716
+ ecmaVersion: 2024,
22717
+ sourceType: "module",
22718
+ childVisitorKeys: RUNTIME_VISITOR_KEYS,
22719
+ fallback: getAstChildKeys
22720
+ }),
22721
+ scopeByNode: /* @__PURE__ */ new WeakMap(),
22722
+ referenceByIdentifier: /* @__PURE__ */ new WeakMap()
22073
22723
  };
22074
22724
  programToAnalysis.set(programNode, analysis);
22075
22725
  return analysis;
22076
22726
  };
22077
- const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
22078
- const getScopeForNode = (node, manager) => {
22727
+ const getScopeForNode = (node, analysis) => {
22079
22728
  if (!node.range) return null;
22080
- let scopeByNode = scopeByNodeCache.get(manager);
22081
- if (!scopeByNode) {
22082
- scopeByNode = /* @__PURE__ */ new WeakMap();
22083
- scopeByNodeCache.set(manager, scopeByNode);
22084
- }
22729
+ const scopeByNode = analysis.scopeByNode;
22085
22730
  if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
22086
22731
  let bestScope = null;
22087
22732
  let bestSize = Infinity;
22088
- for (const scope of manager.scopes) {
22733
+ for (const scope of analysis.scopeManager.scopes) {
22089
22734
  const block = scope.block;
22090
22735
  if (!block?.range) continue;
22091
22736
  if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
@@ -22100,7 +22745,6 @@ const getScopeForNode = (node, manager) => {
22100
22745
  };
22101
22746
  //#endregion
22102
22747
  //#region src/plugin/rules/state-and-effects/utils/effect/ast.ts
22103
- const getChildKeys = (node) => VISITOR_KEYS[node.type] ?? Object.keys(node).filter((key) => key !== "parent");
22104
22748
  const HOOK_NAME_PATTERN$2 = /^use[A-Z0-9]/;
22105
22749
  const isInsideCallbackArgumentOf = (identifier, initializer) => {
22106
22750
  if (!isNodeOfType(initializer, "CallExpression") && !isNodeOfType(initializer, "NewExpression")) return false;
@@ -22136,7 +22780,7 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
22136
22780
  if (visited.has(node)) return;
22137
22781
  visit(node);
22138
22782
  visited.add(node);
22139
- const keys = getChildKeys(node);
22783
+ const keys = getAstChildKeys(node);
22140
22784
  const record = node;
22141
22785
  for (const key of keys) {
22142
22786
  const child = record[key];
@@ -22169,16 +22813,11 @@ const findDownstreamNodes = (topNode, type) => {
22169
22813
  });
22170
22814
  return nodes;
22171
22815
  };
22172
- const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
22173
22816
  const getRef = (analysis, identifier) => {
22174
- let refByIdentifier = refByIdentifierCache.get(analysis);
22175
- if (!refByIdentifier) {
22176
- refByIdentifier = /* @__PURE__ */ new WeakMap();
22177
- refByIdentifierCache.set(analysis, refByIdentifier);
22178
- }
22817
+ const refByIdentifier = analysis.referenceByIdentifier;
22179
22818
  if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
22180
22819
  let resolvedReference = null;
22181
- const scope = getScopeForNode(identifier, analysis.scopeManager);
22820
+ const scope = getScopeForNode(identifier, analysis);
22182
22821
  if (scope) {
22183
22822
  for (const reference of scope.references) if (reference.identifier === identifier) {
22184
22823
  resolvedReference = reference;
@@ -22426,7 +23065,7 @@ const isReactFunctionalHOC = (analysis, node) => {
22426
23065
  const isWrappedSeparately = () => {
22427
23066
  if (!isNodeOfType(node.id, "Identifier")) return false;
22428
23067
  const bindingName = node.id.name;
22429
- const containingScope = getScopeForNode(node, analysis.scopeManager);
23068
+ const containingScope = getScopeForNode(node, analysis);
22430
23069
  if (!containingScope) return false;
22431
23070
  const variable = containingScope.variables.find((v) => v.name === bindingName);
22432
23071
  if (!variable) return false;
@@ -22577,10 +23216,15 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
22577
23216
  if (!declaringNode) return false;
22578
23217
  return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
22579
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
+ };
22580
23225
  const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
22581
23226
  if (def.type !== "Parameter") return false;
22582
- const bindingParent = def.name.parent;
22583
- return isFunctionLike$1(bindingParent);
23227
+ return isWholePropsParameterBinding(def.name);
22584
23228
  }));
22585
23229
  const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
22586
23230
  const isPropAlias = (analysis, ref) => {
@@ -22657,11 +23301,14 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
22657
23301
  if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
22658
23302
  if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$1(node)) return false;
22659
23303
  const record = node;
22660
- for (const [key, value] of Object.entries(record)) {
22661
- if (key === "parent") continue;
22662
- if (Array.isArray(value)) {
22663
- if (value.some((item) => isAstNode(item) && hasCleanupReturn(analysis, item, visited))) return true;
22664
- } else if (isAstNode(value) && hasCleanupReturn(analysis, value, visited)) return true;
23304
+ const childKeys = getAstChildKeys(node);
23305
+ for (let keyIndex = 0; keyIndex < childKeys.length; keyIndex += 1) {
23306
+ const value = record[childKeys[keyIndex]];
23307
+ if (Array.isArray(value)) for (let itemIndex = 0; itemIndex < value.length; itemIndex += 1) {
23308
+ const item = value[itemIndex];
23309
+ if (isAstNode(item) && hasCleanupReturn(analysis, item, visited)) return true;
23310
+ }
23311
+ else if (isAstNode(value) && hasCleanupReturn(analysis, value, visited)) return true;
22665
23312
  }
22666
23313
  return false;
22667
23314
  };
@@ -22889,61 +23536,73 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
22889
23536
  "Array",
22890
23537
  "BigInt",
22891
23538
  "Boolean",
23539
+ "encodeURIComponent",
22892
23540
  "Number",
22893
23541
  "Object",
22894
23542
  "String",
22895
23543
  "parseFloat",
22896
- "parseInt"
23544
+ "parseInt",
23545
+ "structuredClone"
23546
+ ]);
23547
+ const PURE_GLOBAL_CONSTRUCTOR_NAMES = new Set(["Date", "Set"]);
23548
+ const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([
23549
+ ["Array", new Set(["from"])],
23550
+ ["JSON", new Set([
23551
+ "isRawJSON",
23552
+ "parse",
23553
+ "rawJSON",
23554
+ "stringify"
23555
+ ])],
23556
+ ["Math", new Set([
23557
+ "abs",
23558
+ "acos",
23559
+ "acosh",
23560
+ "asin",
23561
+ "asinh",
23562
+ "atan",
23563
+ "atan2",
23564
+ "atanh",
23565
+ "cbrt",
23566
+ "ceil",
23567
+ "clz32",
23568
+ "cos",
23569
+ "cosh",
23570
+ "exp",
23571
+ "floor",
23572
+ "fround",
23573
+ "hypot",
23574
+ "imul",
23575
+ "log",
23576
+ "log10",
23577
+ "log1p",
23578
+ "log2",
23579
+ "max",
23580
+ "min",
23581
+ "pow",
23582
+ "round",
23583
+ "sign",
23584
+ "sin",
23585
+ "sinh",
23586
+ "sqrt",
23587
+ "tan",
23588
+ "tanh",
23589
+ "trunc"
23590
+ ])],
23591
+ ["Object", new Set(["assign"])]
22897
23592
  ]);
22898
- const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
22899
- const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
22900
- "isRawJSON",
22901
- "parse",
22902
- "rawJSON",
22903
- "stringify"
22904
- ])], ["Math", new Set([
22905
- "abs",
22906
- "acos",
22907
- "acosh",
22908
- "asin",
22909
- "asinh",
22910
- "atan",
22911
- "atan2",
22912
- "atanh",
22913
- "cbrt",
22914
- "ceil",
22915
- "clz32",
22916
- "cos",
22917
- "cosh",
22918
- "exp",
22919
- "floor",
22920
- "fround",
22921
- "hypot",
22922
- "imul",
22923
- "log",
22924
- "log10",
22925
- "log1p",
22926
- "log2",
22927
- "max",
22928
- "min",
22929
- "pow",
22930
- "round",
22931
- "sign",
22932
- "sin",
22933
- "sinh",
22934
- "sqrt",
22935
- "tan",
22936
- "tanh",
22937
- "trunc"
22938
- ])]]);
22939
23593
  const PURE_MEMBER_TRANSFORM_NAMES = new Set([
22940
23594
  "concat",
22941
23595
  "filter",
23596
+ "flatMap",
22942
23597
  "join",
22943
23598
  "map",
23599
+ "reduce",
23600
+ "replace",
23601
+ "slice",
22944
23602
  "split",
22945
23603
  "toLowerCase",
22946
23604
  "toString",
23605
+ "toSorted",
22947
23606
  "toUpperCase",
22948
23607
  "trim"
22949
23608
  ]);
@@ -22952,7 +23611,7 @@ const STORAGE_GLOBAL_NAMES$1 = new Set([
22952
23611
  "localStorage",
22953
23612
  "sessionStorage"
22954
23613
  ]);
22955
- const getStaticMemberName = (node) => {
23614
+ const getStaticMemberName$1 = (node) => {
22956
23615
  if (!isNodeOfType(node, "MemberExpression")) return null;
22957
23616
  if (!node.computed && isNodeOfType(node.property, "Identifier")) return node.property.name;
22958
23617
  if (node.computed && isNodeOfType(node.property, "Literal")) return typeof node.property.value === "string" ? node.property.value : null;
@@ -22967,7 +23626,7 @@ const getCallCalleeName$1 = (callExpression) => {
22967
23626
  if (!isNodeOfType(callExpression, "CallExpression")) return null;
22968
23627
  const callee = stripParenExpression(callExpression.callee);
22969
23628
  if (isNodeOfType(callee, "Identifier")) return callee.name;
22970
- return getStaticMemberName(callee);
23629
+ return getStaticMemberName$1(callee);
22971
23630
  };
22972
23631
  const getFunctionParameters = (functionNode) => isFunctionLike$1(functionNode) ? functionNode.params ?? [] : [];
22973
23632
  const getIdentifierBindingIdentity = (analysis, identifier) => {
@@ -23083,14 +23742,14 @@ const localUseEventPreservesCallback = (analysis, callee) => {
23083
23742
  if (!isNodeOfType(child, "CallExpression")) return;
23084
23743
  const forwardedCallee = stripParenExpression(child.callee);
23085
23744
  if (!isNodeOfType(forwardedCallee, "MemberExpression")) return;
23086
- if (getStaticMemberName(forwardedCallee) !== "current") return;
23745
+ if (getStaticMemberName$1(forwardedCallee) !== "current") return;
23087
23746
  if (!isNodeOfType(forwardedCallee.object, "Identifier")) return;
23088
23747
  const refReference = getRef(analysis, forwardedCallee.object);
23089
23748
  if (!callbackRefDeclarators.find((declarator) => refReference?.resolved?.defs.some((definition) => definition.node === declarator)) || !refReference?.resolved) return;
23090
23749
  if (!refReference.resolved.references.some((candidateReference) => {
23091
23750
  const memberExpression = candidateReference.identifier.parent;
23092
23751
  const assignmentExpression = memberExpression?.parent;
23093
- if (!memberExpression || !isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName(memberExpression) !== "current" || !assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression") || assignmentExpression.left !== memberExpression) return false;
23752
+ if (!memberExpression || !isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName$1(memberExpression) !== "current" || !assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression") || assignmentExpression.left !== memberExpression) return false;
23094
23753
  return getIdentifierBindingIdentity(analysis, assignmentExpression.right) !== callbackBinding;
23095
23754
  })) forwardsCallback = true;
23096
23755
  });
@@ -23115,7 +23774,7 @@ const resolveWrappedCallable = (analysis, node) => {
23115
23774
  return callback && isFunctionLike$1(callback) ? callback : null;
23116
23775
  }
23117
23776
  if (!isNodeOfType(candidate, "MemberExpression")) return null;
23118
- if (getStaticMemberName(candidate) !== "current") return null;
23777
+ if (getStaticMemberName$1(candidate) !== "current") return null;
23119
23778
  if (!isNodeOfType(candidate.object, "Identifier")) return null;
23120
23779
  const reference = getRef(analysis, candidate.object);
23121
23780
  const declarator = reference?.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
@@ -23127,7 +23786,7 @@ const resolveWrappedCallable = (analysis, node) => {
23127
23786
  return Boolean(reference?.resolved?.references.some((candidateReference) => {
23128
23787
  const member = candidateReference.identifier.parent;
23129
23788
  const assignment = member?.parent;
23130
- return Boolean(member && isNodeOfType(member, "MemberExpression") && getStaticMemberName(member) === "current" && assignment && isNodeOfType(assignment, "AssignmentExpression") && assignment.left === member);
23789
+ return Boolean(member && isNodeOfType(member, "MemberExpression") && getStaticMemberName$1(member) === "current" && assignment && isNodeOfType(assignment, "AssignmentExpression") && assignment.left === member);
23131
23790
  })) ? null : initializer;
23132
23791
  };
23133
23792
  const functionInvokesItself = (analysis, functionNode) => {
@@ -23166,7 +23825,7 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilena
23166
23825
  if (!isNodeOfType(child, "CallExpression")) return;
23167
23826
  const callee = stripParenExpression(child.callee);
23168
23827
  const calleeName = getCallCalleeName$1(child);
23169
- const memberName = getStaticMemberName(callee);
23828
+ const memberName = getStaticMemberName$1(callee);
23170
23829
  const isDeferringCall = calleeName !== null && DEFERRING_CALLEE_NAMES.has(calleeName) || memberName !== null && DEFERRING_MEMBER_NAMES.has(memberName);
23171
23830
  const isIteratorCall = memberName !== null && SYNCHRONOUS_ITERATOR_METHOD_NAMES$1.has(memberName) && isNodeOfType(callee, "MemberExpression");
23172
23831
  const enqueueFrame = (callableNode, argumentsForCallable, isDeferred, introducedBindings, allowWithoutInvocationEvidence = false) => {
@@ -23325,14 +23984,19 @@ const analyzeHelperExpression = (expression, environment, usedParameterIndices)
23325
23984
  const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
23326
23985
  return callbackSummary.isValid && !callbackSummary.canContinue;
23327
23986
  }
23987
+ if (isNodeOfType(node, "NewExpression")) {
23988
+ const callee = stripParenExpression(node.callee);
23989
+ if (!isNodeOfType(callee, "Identifier") || !PURE_GLOBAL_CONSTRUCTOR_NAMES.has(callee.name) || environment.parameterIndices.has(callee.name) || environment.recursiveNames.has(callee.name) || environment.shadowedGlobalNames.has(callee.name)) return false;
23990
+ return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
23991
+ }
23328
23992
  if (isNodeOfType(node, "CallExpression")) {
23329
23993
  const callee = stripParenExpression(node.callee);
23330
23994
  const calleeRoot = getMemberRoot(callee);
23331
23995
  const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && !environment.parameterIndices.has(callee.name) && !environment.recursiveNames.has(callee.name) && !environment.shadowedGlobalNames.has(callee.name);
23332
23996
  const namespaceName = isNodeOfType(calleeRoot, "Identifier") && !environment.parameterIndices.has(calleeRoot.name) && !environment.recursiveNames.has(calleeRoot.name) && !environment.shadowedGlobalNames.has(calleeRoot.name) ? calleeRoot.name : null;
23333
- const namespaceMemberName = getStaticMemberName(callee);
23997
+ const namespaceMemberName = getStaticMemberName$1(callee);
23334
23998
  const isPureNamespaceCall = isNodeOfType(callee, "MemberExpression") && namespaceName !== null && namespaceMemberName !== null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(namespaceName)?.has(namespaceMemberName) === true;
23335
- const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
23999
+ const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
23336
24000
  if (!isPureGlobalCall && !isPureNamespaceCall && !isPureMemberTransform) return false;
23337
24001
  if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression") && !analyzeHelperExpression(callee.object, environment, usedParameterIndices)) return false;
23338
24002
  return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
@@ -23406,7 +24070,7 @@ const isLocallyConstructedObjectMember = (reference, memberExpression) => isNode
23406
24070
  return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
23407
24071
  }) === true;
23408
24072
  const getUseRefDeclarator = (analysis, memberExpression) => {
23409
- if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName(memberExpression) !== "current" || !isNodeOfType(memberExpression.object, "Identifier")) return null;
24073
+ if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName$1(memberExpression) !== "current" || !isNodeOfType(memberExpression.object, "Identifier")) return null;
23410
24074
  return getRef(analysis, memberExpression.object)?.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator") && isNodeOfType(definitionNode.init, "CallExpression") && getCallCalleeName$1(definitionNode.init) === "useRef") ?? null;
23411
24075
  };
23412
24076
  const collectValueEvidence = (analysis, expression, frame, remainingCallFrames, visitedBindings = /* @__PURE__ */ new Set()) => {
@@ -23475,7 +24139,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23475
24139
  return collectValueEvidence(analysis, initializer.init, frame, remainingCallFrames, visitedBindings);
23476
24140
  }
23477
24141
  if (isNodeOfType(node, "MemberExpression")) {
23478
- if (getStaticMemberName(node) === "current" && isNodeOfType(node.object, "Identifier")) {
24142
+ if (getStaticMemberName$1(node) === "current" && isNodeOfType(node.object, "Identifier")) {
23479
24143
  const refBinding = getRef(analysis, node.object)?.resolved;
23480
24144
  const refDeclarator = useRefDeclarator;
23481
24145
  if (refBinding && refDeclarator && isNodeOfType(refDeclarator, "VariableDeclarator") && isNodeOfType(refDeclarator.init, "CallExpression")) {
@@ -23491,7 +24155,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23491
24155
  if (candidateReference.init) continue;
23492
24156
  const identifier = candidateReference.identifier;
23493
24157
  const member = identifier.parent;
23494
- if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== identifier || getStaticMemberName(member) !== "current") {
24158
+ if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== identifier || getStaticMemberName$1(member) !== "current") {
23495
24159
  evidence.hasUnknownSource = true;
23496
24160
  continue;
23497
24161
  }
@@ -23527,8 +24191,8 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23527
24191
  }
23528
24192
  const callee = stripParenExpression(node.callee);
23529
24193
  const calleeRoot = getMemberRoot(callee);
23530
- const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(calleeRoot, "Identifier") && PURE_GLOBAL_NAMESPACE_NAMES.has(calleeRoot.name) && getIdentifierBindingIdentity(analysis, calleeRoot) === null;
23531
- const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
24194
+ const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(callee, "MemberExpression") && isNodeOfType(calleeRoot, "Identifier") && getIdentifierBindingIdentity(analysis, calleeRoot) === null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(calleeRoot.name)?.has(getStaticMemberName$1(callee) ?? "") === true;
24195
+ const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
23532
24196
  if (isPureGlobalCall || isPureMemberTransform) {
23533
24197
  if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
23534
24198
  for (const argument of node.arguments ?? []) {
@@ -23580,6 +24244,15 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23580
24244
  }
23581
24245
  return evidence;
23582
24246
  }
24247
+ if (isNodeOfType(node, "NewExpression")) {
24248
+ const callee = stripParenExpression(node.callee);
24249
+ if (!isNodeOfType(callee, "Identifier") || !PURE_GLOBAL_CONSTRUCTOR_NAMES.has(callee.name) || getIdentifierBindingIdentity(analysis, callee) !== null) {
24250
+ evidence.hasUnknownSource = true;
24251
+ return evidence;
24252
+ }
24253
+ for (const argument of node.arguments ?? []) mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
24254
+ return evidence;
24255
+ }
23583
24256
  if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
23584
24257
  evidence.hasUnknownSource = true;
23585
24258
  return evidence;
@@ -23998,6 +24671,26 @@ const STATEFUL_HTML_DESCENDANT_TAGS = new Set([
23998
24671
  "dialog",
23999
24672
  "canvas"
24000
24673
  ]);
24674
+ const STATEFUL_HTML_ATTRIBUTE_NAMES = new Set([
24675
+ "autofocus",
24676
+ "contenteditable",
24677
+ "draggable",
24678
+ "tabindex"
24679
+ ]);
24680
+ const isStaticallyFalseAttributeValue = (attribute) => {
24681
+ if (!isNodeOfType(attribute, "JSXAttribute") || !attribute.value) return false;
24682
+ const value = isNodeOfType(attribute.value, "JSXExpressionContainer") ? attribute.value.expression : attribute.value;
24683
+ return isNodeOfType(value, "Literal") && (value.value === false || value.value === "false");
24684
+ };
24685
+ const hasStatefulHtmlAttribute = (openingElement) => {
24686
+ if (!isNodeOfType(openingElement, "JSXOpeningElement")) return false;
24687
+ return openingElement.attributes.some((attribute) => {
24688
+ if (!isNodeOfType(attribute, "JSXAttribute") || !isNodeOfType(attribute.name, "JSXIdentifier")) return false;
24689
+ const attributeName = attribute.name.name.toLowerCase();
24690
+ if (!STATEFUL_HTML_ATTRIBUTE_NAMES.has(attributeName)) return false;
24691
+ return attributeName === "tabindex" || !isStaticallyFalseAttributeValue(attribute);
24692
+ });
24693
+ };
24001
24694
  const STATEFUL_DESCENDANT_SCAN_BUDGET = 200;
24002
24695
  const rootIdentifierNameOf = (expression) => {
24003
24696
  let object = expression;
@@ -24015,7 +24708,8 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
24015
24708
  budget -= 1;
24016
24709
  const node = stack.pop();
24017
24710
  if (isNodeOfType(node, "JSXElement")) {
24018
- const name = node.openingElement.name;
24711
+ const opening = node.openingElement;
24712
+ const name = opening.name;
24019
24713
  if (name && isNodeOfType(name, "JSXIdentifier")) {
24020
24714
  const tagName = name.name;
24021
24715
  const firstChar = tagName.charCodeAt(0);
@@ -24023,6 +24717,7 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
24023
24717
  if (STATEFUL_HTML_DESCENDANT_TAGS.has(tagName)) return true;
24024
24718
  }
24025
24719
  if (name && isNodeOfType(name, "JSXMemberExpression")) return true;
24720
+ if (hasStatefulHtmlAttribute(opening)) return true;
24026
24721
  const children = node.children ?? [];
24027
24722
  for (const child of children) stack.push(child);
24028
24723
  continue;
@@ -24116,6 +24811,7 @@ const ARITHMETIC_KEY_OPERATORS = new Set([
24116
24811
  "/",
24117
24812
  "%"
24118
24813
  ]);
24814
+ const bindingMutationResultsByProgram = /* @__PURE__ */ new WeakMap();
24119
24815
  const extractCandidateIdentifiers = (expression) => {
24120
24816
  const node = stripParenExpression(expression);
24121
24817
  if (isNodeOfType(node, "Identifier")) return [node];
@@ -24156,6 +24852,13 @@ const isArrayFromCall = (node) => {
24156
24852
  const isBindingReassignedOrMutated = (referenceNode, bindingName) => {
24157
24853
  const programRoot = findProgramRoot(referenceNode);
24158
24854
  if (!programRoot) return false;
24855
+ let mutationResultsByBindingName = bindingMutationResultsByProgram.get(programRoot);
24856
+ if (!mutationResultsByBindingName) {
24857
+ mutationResultsByBindingName = /* @__PURE__ */ new Map();
24858
+ bindingMutationResultsByProgram.set(programRoot, mutationResultsByBindingName);
24859
+ }
24860
+ const cachedResult = mutationResultsByBindingName.get(bindingName);
24861
+ if (cachedResult !== void 0) return cachedResult;
24159
24862
  let didFindWrite = false;
24160
24863
  walkAst(programRoot, (child) => {
24161
24864
  if (didFindWrite) return false;
@@ -24172,6 +24875,7 @@ const isBindingReassignedOrMutated = (referenceNode, bindingName) => {
24172
24875
  return false;
24173
24876
  }
24174
24877
  });
24878
+ mutationResultsByBindingName.set(bindingName, didFindWrite);
24175
24879
  return didFindWrite;
24176
24880
  };
24177
24881
  /**
@@ -24573,6 +25277,14 @@ const templateHasOuterMemberIdentity = (template, bindingFunction) => {
24573
25277
  }
24574
25278
  return false;
24575
25279
  };
25280
+ const findBareItemNamesReferencedByTemplate = (template, itemNames) => {
25281
+ const referencedItemNames = /* @__PURE__ */ new Set();
25282
+ for (const expression of template.expressions ?? []) {
25283
+ const unwrappedExpression = stripParenExpression(expression);
25284
+ if (isNodeOfType(unwrappedExpression, "Identifier") && itemNames.has(unwrappedExpression.name)) referencedItemNames.add(unwrappedExpression.name);
25285
+ }
25286
+ return referencedItemNames;
25287
+ };
24576
25288
  const forLoopTestReadsDataLength = (test) => {
24577
25289
  let didFindLengthRead = false;
24578
25290
  walkAst(test, (child) => {
@@ -24709,9 +25421,11 @@ const noArrayIndexAsKey = defineRule({
24709
25421
  } else if (STATELESS_HTML_LEAF_TAGS.has(elementName.name)) {
24710
25422
  const jsxElement = openingElement.parent;
24711
25423
  if (jsxElement && isNodeOfType(jsxElement, "JSXElement")) {
25424
+ const isInlineTextRun = INLINE_TEXT_LEAF_TAGS.has(elementName.name);
25425
+ const primitiveItemNames = keyTemplate ? findBareItemNamesReferencedByTemplate(keyTemplate, itemNames) : EMPTY_NAME_SET;
24712
25426
  if (!containsStatefulDescendant(jsxElement, {
24713
- memberRootNames: INLINE_TEXT_LEAF_TAGS.has(elementName.name) ? itemNames : EMPTY_NAME_SET,
24714
- bareIdentifierNames: derivedNames
25427
+ memberRootNames: isInlineTextRun ? itemNames : EMPTY_NAME_SET,
25428
+ bareIdentifierNames: primitiveItemNames.size > 0 ? new Set([...derivedNames, ...primitiveItemNames]) : derivedNames
24715
25429
  })) return;
24716
25430
  }
24717
25431
  }
@@ -24879,7 +25593,11 @@ const noAsyncEffectCallback = defineRule({
24879
25593
  severity: "warn",
24880
25594
  recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
24881
25595
  create: (context) => ({ CallExpression(node) {
24882
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
25596
+ if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
25597
+ allowGlobalReactNamespace: true,
25598
+ allowUnboundBareCalls: true,
25599
+ resolveNamedAliases: true
25600
+ })) return;
24883
25601
  const callback = getEffectCallback(node);
24884
25602
  if (!callback) return;
24885
25603
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
@@ -25272,10 +25990,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
25272
25990
  const section = manifest[sectionName];
25273
25991
  return typeof section === "object" && section !== null && Object.keys(section).length > 0;
25274
25992
  });
25275
- const declaresDependency = (manifest, dependencyName) => {
25276
- for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
25277
- return false;
25278
- };
25993
+ const declaresDependency = (manifest, dependencyName) => DEPENDENCY_SECTION_NAMES.some((sectionName) => {
25994
+ const section = manifest[sectionName];
25995
+ return typeof section === "object" && section !== null && Object.prototype.propertyIsEnumerable.call(section, dependencyName);
25996
+ });
25279
25997
  const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
25280
25998
  const classifyPackagePlatform = (filename) => {
25281
25999
  const manifest = readNearestPackageManifest(filename);
@@ -25503,6 +26221,56 @@ const noBarrelImport = defineRule({
25503
26221
  }
25504
26222
  });
25505
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
25506
26274
  //#region src/plugin/utils/function-contains-react-render-output.ts
25507
26275
  const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
25508
26276
  "FunctionDeclaration",
@@ -25522,25 +26290,24 @@ const isCallArgumentFunctionExpression = (node) => {
25522
26290
  return parent.arguments.some((argumentNode) => argumentNode === node);
25523
26291
  };
25524
26292
  const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
25525
- const containsRenderOutput$1 = (node, rootNode, scopes) => {
25526
- if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
25527
- if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
25528
- if (isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) return true;
25529
- const nodeRecord = node;
25530
- for (const key of Object.keys(nodeRecord)) {
25531
- if (key === "parent") continue;
25532
- const child = nodeRecord[key];
25533
- if (Array.isArray(child)) {
25534
- for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput$1(innerChild, rootNode, scopes)) return true;
25535
- } else if (isAstNode(child) && containsRenderOutput$1(child, rootNode, scopes)) return true;
25536
- }
25537
- return false;
26293
+ const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
26294
+ const containsRenderOutput$1 = (rootNode, scopes) => {
26295
+ let hasRenderOutput = false;
26296
+ walkAst(rootNode, (node) => {
26297
+ if (hasRenderOutput) return false;
26298
+ if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
26299
+ if (isRenderOutputExpression(node, scopes)) {
26300
+ hasRenderOutput = true;
26301
+ return false;
26302
+ }
26303
+ });
26304
+ return hasRenderOutput;
25538
26305
  };
25539
26306
  const renderOutputCache = /* @__PURE__ */ new WeakMap();
25540
26307
  const functionContainsReactRenderOutput = (functionNode, scopes) => {
25541
26308
  const cachedEntry = renderOutputCache.get(functionNode);
25542
26309
  if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
25543
- const hasRenderOutput = containsRenderOutput$1(functionNode, functionNode, scopes);
26310
+ const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
25544
26311
  renderOutputCache.set(functionNode, {
25545
26312
  scopes,
25546
26313
  hasRenderOutput
@@ -25548,6 +26315,9 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
25548
26315
  return hasRenderOutput;
25549
26316
  };
25550
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
25551
26321
  //#region src/plugin/utils/is-component-declaration.ts
25552
26322
  const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
25553
26323
  //#endregion
@@ -26079,6 +26849,19 @@ const noCloneElement = defineRule({
26079
26849
  } })
26080
26850
  });
26081
26851
  //#endregion
26852
+ //#region src/plugin/utils/get-call-method-name.ts
26853
+ /**
26854
+ * Returns the static method name of a call's callee when it's a
26855
+ * non-computed MemberExpression (`obj.method` → `"method"`), or
26856
+ * `null` otherwise. Used by `no-pass-data-to-parent` and
26857
+ * `no-pass-live-state-to-parent` to match the receiver-bound
26858
+ * method-call shape against an allow/block list.
26859
+ */
26860
+ const getCallMethodName = (callee) => {
26861
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
26862
+ return null;
26863
+ };
26864
+ //#endregion
26082
26865
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
26083
26866
  const MESSAGE$31 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
26084
26867
  const CONTEXT_MODULES = [
@@ -26086,23 +26869,35 @@ const CONTEXT_MODULES = [
26086
26869
  "use-context-selector",
26087
26870
  "react-tracked"
26088
26871
  ];
26089
- const isCreateContextCallee = (callee) => {
26872
+ const getSupportedContextImportSource = (symbol) => {
26873
+ if (symbol?.kind !== "import") return null;
26874
+ const importDeclaration = symbol.declarationNode.parent;
26875
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration") || typeof importDeclaration.source.value !== "string" || !CONTEXT_MODULES.includes(importDeclaration.source.value)) return null;
26876
+ return importDeclaration.source.value;
26877
+ };
26878
+ const isSupportedNamespaceSymbol = (symbol) => getSupportedContextImportSource(symbol) !== null && Boolean(symbol && (isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")));
26879
+ const isDestructuredCreateContextBinding = (identifier, scopes) => {
26880
+ if (!isNodeOfType(identifier, "Identifier")) return false;
26881
+ const symbol = scopes.symbolFor(identifier);
26882
+ if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
26883
+ const property = symbol.bindingIdentifier.parent;
26884
+ if (!property || !isNodeOfType(property, "Property") || getStaticPropertyKeyName(property, { allowComputedString: true }) !== "createContext") return false;
26885
+ const initializer = stripParenExpression(symbol.initializer);
26886
+ return isNodeOfType(initializer, "Identifier") && isSupportedNamespaceSymbol(resolveConstIdentifierAlias(initializer, scopes));
26887
+ };
26888
+ const isCreateContextCall = (node, scopes) => {
26889
+ const callee = stripParenExpression(node.callee);
26090
26890
  if (isNodeOfType(callee, "Identifier")) {
26091
- const binding = getImportBindingForName(callee, callee.name);
26092
- return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
26891
+ if (isDestructuredCreateContextBinding(callee, scopes)) return true;
26892
+ const symbol = resolveConstIdentifierAlias(callee, scopes);
26893
+ return getSupportedContextImportSource(symbol) !== null && Boolean(symbol && getImportedName(symbol.declarationNode) === "createContext");
26093
26894
  }
26094
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
26095
- const namespaceIdentifier = callee.object;
26096
- const propertyIdentifier = callee.property;
26097
- if (!isNodeOfType(namespaceIdentifier, "Identifier")) return false;
26098
- if (!isNodeOfType(propertyIdentifier, "Identifier")) return false;
26099
- if (propertyIdentifier.name !== "createContext") return false;
26100
- const namespaceName = namespaceIdentifier.name;
26101
- if (isCanonicalReactNamespaceName(namespaceName)) return true;
26102
- const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
26103
- return importSource !== null && CONTEXT_MODULES.includes(importSource);
26104
- }
26105
- return false;
26895
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
26896
+ if ((getCallMethodName(callee) ?? (callee.computed && isNodeOfType(callee.property, "Literal") && typeof callee.property.value === "string" ? callee.property.value : null)) !== "createContext") return false;
26897
+ const receiver = stripParenExpression(callee.object);
26898
+ if (!isNodeOfType(receiver, "Identifier")) return false;
26899
+ if (receiver.name === "React" && scopes.isGlobalReference(receiver)) return true;
26900
+ return isSupportedNamespaceSymbol(resolveConstIdentifierAlias(receiver, scopes));
26106
26901
  };
26107
26902
  const noCreateContextInRender = defineRule({
26108
26903
  id: "no-create-context-in-render",
@@ -26111,7 +26906,7 @@ const noCreateContextInRender = defineRule({
26111
26906
  category: "Correctness",
26112
26907
  recommendation: "Move `createContext(...)` outside the component, to the top level of the file, so it stays the same on every render.",
26113
26908
  create: (context) => ({ CallExpression(node) {
26114
- if (!isCreateContextCallee(node.callee)) return;
26909
+ if (!isCreateContextCall(node, context.scopes)) return;
26115
26910
  const componentOrHookName = enclosingComponentOrHookName(node);
26116
26911
  if (!componentOrHookName) return;
26117
26912
  context.report({
@@ -27346,7 +28141,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
27346
28141
  let ancestor = setStateCall.parent;
27347
28142
  while (ancestor) {
27348
28143
  if (!lifecycleMember) {
27349
- if (FUNCTION_NODE_TYPES$1.has(ancestor.type)) nestedFunctionCount += 1;
28144
+ if (FUNCTION_NODE_TYPES$1.has(ancestor.type) && (!isImmediatelyInvokedFunction(ancestor) || !("async" in ancestor) || ancestor.async === true)) nestedFunctionCount += 1;
27350
28145
  if (isLifecycleMember(ancestor, lifecycleNames)) lifecycleMember = ancestor;
27351
28146
  } else if (isEs5Component(ancestor) || isEs6Component(ancestor)) {
27352
28147
  if (nestedFunctionCount > 1 && !options.disallowInNestedFunctions) return false;
@@ -27548,7 +28343,7 @@ const collectDiffSourceLocalNames = (lifecycleFunction, paramNames) => {
27548
28343
  const body = lifecycleFunction.body;
27549
28344
  if (!body) return derivedNames;
27550
28345
  walkAst(body, (node) => {
27551
- if (FUNCTION_NODE_TYPES.has(node.type)) return false;
28346
+ if (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node)) return false;
27552
28347
  if (!isNodeOfType(node, "VariableDeclarator")) return;
27553
28348
  const init = node.init;
27554
28349
  if (!init) return;
@@ -27558,6 +28353,67 @@ const collectDiffSourceLocalNames = (lifecycleFunction, paramNames) => {
27558
28353
  return derivedNames;
27559
28354
  };
27560
28355
  const isStatefulOperand = (node, paramNames, derivedNames) => referencesAnyName(node, paramNames) || referencesAnyName(node, derivedNames) || containsThisStateOrProps(node);
28356
+ const getStaticMemberName = (node) => {
28357
+ if (!isNodeOfType(node, "MemberExpression") || node.computed === true) return null;
28358
+ return isNodeOfType(node.property, "Identifier") ? node.property.name : null;
28359
+ };
28360
+ const getThisStateFieldName = (node) => {
28361
+ const unwrappedNode = stripParenExpression(node);
28362
+ if (!isNodeOfType(unwrappedNode, "MemberExpression")) return null;
28363
+ const object = stripParenExpression(unwrappedNode.object);
28364
+ if (!isNodeOfType(object, "MemberExpression") || !isNodeOfType(stripParenExpression(object.object), "ThisExpression") || getStaticMemberName(object) !== "state") return null;
28365
+ return getStaticMemberName(unwrappedNode);
28366
+ };
28367
+ const collectLocalInitializers = (lifecycleFunction) => {
28368
+ const initializers = /* @__PURE__ */ new Map();
28369
+ const body = lifecycleFunction.body;
28370
+ if (!body) return initializers;
28371
+ walkAst(body, (node) => {
28372
+ if (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node)) return false;
28373
+ if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.init) initializers.set(node.id.name, node.init);
28374
+ });
28375
+ return initializers;
28376
+ };
28377
+ const derivesFromPostMountValue = (node, localInitializers, visitedNames = /* @__PURE__ */ new Set()) => {
28378
+ if (readsPostMountValue(node)) return true;
28379
+ const referencedNames = /* @__PURE__ */ new Set();
28380
+ collectReferenceIdentifierNames(node, referencedNames);
28381
+ for (const referencedName of referencedNames) {
28382
+ if (visitedNames.has(referencedName)) continue;
28383
+ const initializer = localInitializers.get(referencedName);
28384
+ if (!initializer) continue;
28385
+ if (derivesFromPostMountValue(initializer, localInitializers, new Set([...visitedNames, referencedName]))) return true;
28386
+ }
28387
+ return false;
28388
+ };
28389
+ const getSetStateFieldValue = (setStateCall, fieldName) => {
28390
+ if (!isNodeOfType(setStateCall, "CallExpression")) return null;
28391
+ const argument = setStateCall.arguments?.[0];
28392
+ if (!argument || !isNodeOfType(argument, "ObjectExpression")) return null;
28393
+ for (const property of argument.properties ?? []) {
28394
+ if (!isNodeOfType(property, "Property") || property.computed === true) continue;
28395
+ if ((isNodeOfType(property.key, "Identifier") && property.key.name || isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" && property.key.value || null) === fieldName) return property.value;
28396
+ }
28397
+ return null;
28398
+ };
28399
+ const isConvergentPostMountGuard = (test, setStateCall, localInitializers) => {
28400
+ let qualifies = false;
28401
+ walkAst(test, (node) => {
28402
+ if (qualifies) return false;
28403
+ if (!isNodeOfType(node, "BinaryExpression") || !EQUALITY_OPERATORS.has(node.operator)) return;
28404
+ const leftFieldName = getThisStateFieldName(node.left);
28405
+ const rightFieldName = getThisStateFieldName(node.right);
28406
+ const fieldName = leftFieldName ?? rightFieldName;
28407
+ const comparedValue = leftFieldName ? node.right : node.left;
28408
+ if (!fieldName || !leftFieldName && !rightFieldName) return;
28409
+ const assignedValue = getSetStateFieldValue(setStateCall, fieldName);
28410
+ if (!assignedValue || !areExpressionsStructurallyEqual(comparedValue, assignedValue)) return;
28411
+ if (!derivesFromPostMountValue(comparedValue, localInitializers)) return;
28412
+ qualifies = true;
28413
+ return false;
28414
+ });
28415
+ return qualifies;
28416
+ };
27561
28417
  const isDiffGuardTest = (test, paramNames, derivedNames) => {
27562
28418
  if (referencesAnyName(test, paramNames)) return true;
27563
28419
  let qualifies = false;
@@ -27565,7 +28421,7 @@ const isDiffGuardTest = (test, paramNames, derivedNames) => {
27565
28421
  if (qualifies) return false;
27566
28422
  if (!isNodeOfType(node, "BinaryExpression")) return;
27567
28423
  if (!EQUALITY_OPERATORS.has(node.operator)) return;
27568
- if (isStatefulOperand(node.left, paramNames, derivedNames) && isStatefulOperand(node.right, paramNames, derivedNames)) {
28424
+ if (isStatefulOperand(node.left, paramNames, derivedNames) && isStatefulOperand(node.right, paramNames, derivedNames) && (referencesAnyName(node.left, derivedNames) || referencesAnyName(node.right, derivedNames))) {
27569
28425
  qualifies = true;
27570
28426
  return false;
27571
28427
  }
@@ -27578,11 +28434,12 @@ const isInsideDiffGuard = (setStateCall) => {
27578
28434
  const paramNames = /* @__PURE__ */ new Set();
27579
28435
  for (const param of lifecycleFunction.params ?? []) collectPatternNames(param, paramNames);
27580
28436
  const derivedNames = collectDiffSourceLocalNames(lifecycleFunction, paramNames);
28437
+ const localInitializers = collectLocalInitializers(lifecycleFunction);
27581
28438
  let child = setStateCall;
27582
28439
  let ancestor = setStateCall.parent;
27583
28440
  while (ancestor && ancestor !== lifecycleFunction) {
27584
28441
  const guardTest = isNodeOfType(ancestor, "IfStatement") && child !== ancestor.test && ancestor.test || isNodeOfType(ancestor, "ConditionalExpression") && child !== ancestor.test && ancestor.test || isNodeOfType(ancestor, "LogicalExpression") && ancestor.operator === "&&" && child === ancestor.right && ancestor.left || null;
27585
- if (guardTest && isDiffGuardTest(guardTest, paramNames, derivedNames)) return true;
28442
+ if (guardTest && (isDiffGuardTest(guardTest, paramNames, derivedNames) || isConvergentPostMountGuard(guardTest, setStateCall, localInitializers))) return true;
27586
28443
  child = ancestor;
27587
28444
  ancestor = ancestor.parent ?? null;
27588
28445
  }
@@ -27763,7 +28620,16 @@ const producesOpaqueInstanceValue = (expression) => {
27763
28620
  const collectSetterValueObservations = (componentBody, setterNames) => {
27764
28621
  const plainFedSetterNames = /* @__PURE__ */ new Set();
27765
28622
  const opaqueFedSetterNames = /* @__PURE__ */ new Set();
28623
+ const callbackRefSetterNames = /* @__PURE__ */ new Set();
27766
28624
  walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
28625
+ if (isNodeOfType(node, "JSXAttribute")) {
28626
+ const attributeName = node.name;
28627
+ if (isNodeOfType(attributeName, "JSXIdentifier") && attributeName.name === "ref" && node.value && isNodeOfType(node.value, "JSXExpressionContainer")) {
28628
+ const expression = stripParenExpression(node.value.expression);
28629
+ if (isNodeOfType(expression, "Identifier") && setterNames.has(expression.name)) callbackRefSetterNames.add(expression.name);
28630
+ }
28631
+ return;
28632
+ }
27767
28633
  if (!isNodeOfType(node, "CallExpression")) return;
27768
28634
  if (!isNodeOfType(node.callee, "Identifier")) return;
27769
28635
  const setterName = node.callee.name;
@@ -27780,21 +28646,10 @@ const collectSetterValueObservations = (componentBody, setterNames) => {
27780
28646
  }, true);
27781
28647
  return {
27782
28648
  plainFedSetterNames,
27783
- opaqueFedSetterNames
28649
+ opaqueFedSetterNames,
28650
+ callbackRefSetterNames
27784
28651
  };
27785
28652
  };
27786
- const collectCallbackRefSetterNames = (componentBody) => {
27787
- const callbackRefSetterNames = /* @__PURE__ */ new Set();
27788
- walkAst(componentBody, (node) => {
27789
- if (!isNodeOfType(node, "JSXAttribute")) return;
27790
- const attributeName = node.name;
27791
- if (isNodeOfType(attributeName, "JSXIdentifier") && attributeName.name === "ref" && node.value && isNodeOfType(node.value, "JSXExpressionContainer")) {
27792
- const expression = stripParenExpression(node.value.expression);
27793
- if (isNodeOfType(expression, "Identifier")) callbackRefSetterNames.add(expression.name);
27794
- }
27795
- });
27796
- return callbackRefSetterNames;
27797
- };
27798
28653
  const collectFunctionLocalBindings = (functionNode) => {
27799
28654
  const localBindings = /* @__PURE__ */ new Set();
27800
28655
  if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return localBindings;
@@ -27806,8 +28661,8 @@ const collectFunctionLocalBindings = (functionNode) => {
27806
28661
  return localBindings;
27807
28662
  };
27808
28663
  const collectBlockScopedBindings = (node) => {
27809
- const blockBindings = /* @__PURE__ */ new Set();
27810
28664
  if (isNodeOfType(node, "BlockStatement")) {
28665
+ const blockBindings = /* @__PURE__ */ new Set();
27811
28666
  for (const statement of node.body ?? []) {
27812
28667
  if (!isNodeOfType(statement, "VariableDeclaration")) continue;
27813
28668
  for (const declarator of statement.declarations ?? []) collectPatternNames(declarator.id, blockBindings);
@@ -27815,17 +28670,22 @@ const collectBlockScopedBindings = (node) => {
27815
28670
  return blockBindings;
27816
28671
  }
27817
28672
  if (isNodeOfType(node, "ForStatement") && isNodeOfType(node.init, "VariableDeclaration")) {
28673
+ const blockBindings = /* @__PURE__ */ new Set();
27818
28674
  for (const declarator of node.init.declarations ?? []) collectPatternNames(declarator.id, blockBindings);
27819
28675
  return blockBindings;
27820
28676
  }
27821
- if ((isNodeOfType(node, "ForOfStatement") || isNodeOfType(node, "ForInStatement")) && isNodeOfType(node.left, "VariableDeclaration")) for (const declarator of node.left.declarations ?? []) collectPatternNames(declarator.id, blockBindings);
27822
- return blockBindings;
28677
+ if ((isNodeOfType(node, "ForOfStatement") || isNodeOfType(node, "ForInStatement")) && isNodeOfType(node.left, "VariableDeclaration")) {
28678
+ const blockBindings = /* @__PURE__ */ new Set();
28679
+ for (const declarator of node.left.declarations ?? []) collectPatternNames(declarator.id, blockBindings);
28680
+ return blockBindings;
28681
+ }
28682
+ return null;
27823
28683
  };
27824
28684
  const walkComponentRespectingShadows = (node, shadowedStateNames, visit, isComponentBodyRoot = false) => {
27825
28685
  if (!node || typeof node !== "object") return;
27826
28686
  let nextShadowedStateNames = shadowedStateNames;
27827
- const localBindings = isComponentBodyRoot ? /* @__PURE__ */ new Set() : isFunctionLike$1(node) ? collectFunctionLocalBindings(node) : collectBlockScopedBindings(node);
27828
- if (localBindings.size > 0) {
28687
+ const localBindings = isComponentBodyRoot ? null : isFunctionLike$1(node) ? collectFunctionLocalBindings(node) : collectBlockScopedBindings(node);
28688
+ if (localBindings && localBindings.size > 0) {
27829
28689
  const merged = new Set(shadowedStateNames);
27830
28690
  for (const localName of localBindings) merged.add(localName);
27831
28691
  nextShadowedStateNames = merged;
@@ -27851,11 +28711,10 @@ const noDirectStateMutation = defineRule({
27851
28711
  const bindings = collectUseStateBindings(componentBody);
27852
28712
  if (bindings.length === 0) return;
27853
28713
  const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
27854
- const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
27855
28714
  const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
27856
28715
  const plainObjectStateValueNames = /* @__PURE__ */ new Set();
27857
28716
  for (const binding of bindings) {
27858
- if (callbackRefSetterNames.has(binding.setterName)) continue;
28717
+ if (setterValueObservations.callbackRefSetterNames.has(binding.setterName)) continue;
27859
28718
  if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
27860
28719
  const initializerArgument = binding.declarator.init.arguments?.[0];
27861
28720
  if (!initializerMarksPlainState(initializerArgument)) continue;
@@ -27916,7 +28775,7 @@ const noDisabledZoom = defineRule({
27916
28775
  category: "Accessibility",
27917
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.",
27918
28777
  create: (context) => ({ JSXOpeningElement(node) {
27919
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
28778
+ if (resolveJsxElementType(node) !== "meta") return;
27920
28779
  const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
27921
28780
  if (!nameAttr?.value) return;
27922
28781
  if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
@@ -27997,20 +28856,31 @@ const noDocumentStartViewTransition = defineRule({
27997
28856
  } })
27998
28857
  });
27999
28858
  //#endregion
28859
+ //#region src/plugin/utils/get-static-property-name.ts
28860
+ const getStaticPropertyName = (memberExpression) => {
28861
+ const property = memberExpression.property;
28862
+ if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
28863
+ if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
28864
+ if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
28865
+ return null;
28866
+ };
28867
+ //#endregion
28000
28868
  //#region src/plugin/rules/js-performance/no-document-write.ts
28001
28869
  const MESSAGE$24 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
28002
28870
  const WRITE_METHODS = new Set(["write", "writeln"]);
28871
+ const isGlobalDocumentReference = (node, context) => node.name === "document" && context.scopes.isGlobalReference(node);
28003
28872
  const noDocumentWrite = defineRule({
28004
28873
  id: "no-document-write",
28005
28874
  title: "document.write/writeln",
28006
28875
  severity: "warn",
28007
28876
  recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
28008
28877
  create: (context) => ({ CallExpression(node) {
28009
- const callee = node.callee;
28010
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
28878
+ const callee = stripParenExpression(node.callee);
28879
+ if (!isNodeOfType(callee, "MemberExpression")) return;
28011
28880
  const receiver = stripParenExpression(callee.object);
28012
- if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
28013
- if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
28881
+ if (!isNodeOfType(receiver, "Identifier") || !isGlobalDocumentReference(receiver, context)) return;
28882
+ const methodName = getStaticPropertyName(callee);
28883
+ if (methodName === null || !WRITE_METHODS.has(methodName)) return;
28014
28884
  context.report({
28015
28885
  node,
28016
28886
  message: MESSAGE$24
@@ -28295,7 +29165,7 @@ const findTopLevelEffectCalls = (componentBody) => {
28295
29165
  if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
28296
29166
  for (const statement of componentBody.body ?? []) {
28297
29167
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
28298
- const expression = statement.expression;
29168
+ const expression = unwrapDiscardedExpression(statement);
28299
29169
  if (!isNodeOfType(expression, "CallExpression")) continue;
28300
29170
  if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
28301
29171
  effectCalls.push(expression);
@@ -28817,6 +29687,14 @@ const noEffectWithFreshDeps = defineRule({
28817
29687
  //#endregion
28818
29688
  //#region src/plugin/rules/security/no-eval.ts
28819
29689
  const SANDBOX_SURFACE_PATH_PATTERN = /(?:^|[/-])sandbox(?:$|[/-])|(?:^|\/)[\w.]+-sandbox(?:\/|\.[cm]?[jt]sx?$)/i;
29690
+ const getExecutableGlobalName = (node, context) => {
29691
+ const expression = stripParenExpression(node);
29692
+ if (isNodeOfType(expression, "Identifier")) return context.scopes.isGlobalReference(expression) ? expression.name : null;
29693
+ if (!isNodeOfType(expression, "MemberExpression")) return null;
29694
+ const receiver = stripParenExpression(expression.object);
29695
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "globalThis" || !context.scopes.isGlobalReference(receiver)) return null;
29696
+ return getStaticPropertyName(expression);
29697
+ };
28820
29698
  const noEval = defineRule({
28821
29699
  id: "no-eval",
28822
29700
  title: "eval() runs untrusted code strings",
@@ -28826,20 +29704,21 @@ const noEval = defineRule({
28826
29704
  if (SANDBOX_SURFACE_PATH_PATTERN.test(normalizeFilename(context.filename ?? ""))) return {};
28827
29705
  return {
28828
29706
  CallExpression(node) {
28829
- if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "eval") {
29707
+ const executableGlobalName = getExecutableGlobalName(node.callee, context);
29708
+ if (executableGlobalName === "eval") {
28830
29709
  context.report({
28831
29710
  node,
28832
29711
  message: "eval() is a code-injection vulnerability: it runs any string as code."
28833
29712
  });
28834
29713
  return;
28835
29714
  }
28836
- if (isNodeOfType(node.callee, "Identifier") && (node.callee.name === "setTimeout" || node.callee.name === "setInterval") && isNodeOfType(node.arguments?.[0], "Literal") && typeof node.arguments[0].value === "string") context.report({
29715
+ if ((executableGlobalName === "setTimeout" || executableGlobalName === "setInterval") && isNodeOfType(node.arguments?.[0], "Literal") && typeof node.arguments[0].value === "string") context.report({
28837
29716
  node,
28838
- message: `Passing a string to ${node.callee.name}() is a code-injection vulnerability, since it runs that string as code.`
29717
+ message: `Passing a string to ${executableGlobalName}() is a code-injection vulnerability, since it runs that string as code.`
28839
29718
  });
28840
29719
  },
28841
29720
  NewExpression(node) {
28842
- if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "Function") {
29721
+ if (getExecutableGlobalName(node.callee, context) === "Function") {
28843
29722
  const onlyArgument = node.arguments.length === 1 ? node.arguments[0] : void 0;
28844
29723
  if (isNodeOfType(onlyArgument, "Literal") && typeof onlyArgument.value === "string" && onlyArgument.value.trim() === "return this") return;
28845
29724
  context.report({
@@ -30098,7 +30977,11 @@ const noFetchInEffect = defineRule({
30098
30977
  severity: "warn",
30099
30978
  recommendation: "Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from `useEffect`.",
30100
30979
  create: (context) => ({ CallExpression(node) {
30101
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
30980
+ if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
30981
+ allowGlobalReactNamespace: true,
30982
+ allowUnboundBareCalls: true,
30983
+ resolveNamedAliases: true
30984
+ })) return;
30102
30985
  const callback = getEffectCallback(node);
30103
30986
  if (!callback) return;
30104
30987
  const analysisFunctions = collectEffectAnalysisFunctions(callback, context);
@@ -31064,7 +31947,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
31064
31947
  severity: "warn",
31065
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.",
31066
31949
  create: (context) => ({ JSXOpeningElement(node) {
31067
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
31950
+ if (resolveJsxElementType(node) !== "img") return;
31068
31951
  const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
31069
31952
  if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
31070
31953
  const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
@@ -31305,7 +32188,7 @@ const noIndeterminateAttribute = defineRule({
31305
32188
  if (classifyReactNativeFileTarget(context) === "react-native") return {};
31306
32189
  return {
31307
32190
  JSXOpeningElement(node) {
31308
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
32191
+ if (resolveJsxElementType(node) !== "input") return;
31309
32192
  let typeAttribute = null;
31310
32193
  let typeAttributeIndex = null;
31311
32194
  let indeterminateAttribute = null;
@@ -31484,11 +32367,12 @@ const isMemoCall = (node) => {
31484
32367
  };
31485
32368
  const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
31486
32369
  const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
31487
- const isInlineReference = (node) => {
31488
- 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";
31489
- if (isNodeOfType(node, "ObjectExpression")) return "objects";
31490
- if (isNodeOfType(node, "ArrayExpression")) return "Arrays";
31491
- 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";
31492
32376
  return null;
31493
32377
  };
31494
32378
  const noInlinePropOnMemoComponent = defineRule({
@@ -31518,7 +32402,7 @@ const noInlinePropOnMemoComponent = defineRule({
31518
32402
  let elementName = null;
31519
32403
  if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
31520
32404
  if (!elementName || !memoizedComponentNames.has(elementName)) return;
31521
- const propType = isInlineReference(node.value.expression);
32405
+ const propType = isInlineReference(node.value.expression, context.scopes);
31522
32406
  if (propType) context.report({
31523
32407
  node: node.value.expression,
31524
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`
@@ -32377,11 +33261,13 @@ const noManyBooleanProps = defineRule({
32377
33261
  };
32378
33262
  const checkComponent = (functionNode, param, body, componentName, reportNode) => {
32379
33263
  if (!param) return;
33264
+ const propsBinding = resolveFirstArgumentBinding(param);
33265
+ if (!propsBinding) return;
32380
33266
  if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
32381
- if (isNodeOfType(param, "ObjectPattern")) {
33267
+ if (isNodeOfType(propsBinding, "ObjectPattern")) {
32382
33268
  const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
32383
33269
  const booleanLikePropNames = [];
32384
- for (const property of param.properties ?? []) {
33270
+ for (const property of propsBinding.properties ?? []) {
32385
33271
  if (!isNodeOfType(property, "Property")) continue;
32386
33272
  const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
32387
33273
  if (!keyName) continue;
@@ -32392,7 +33278,7 @@ const noManyBooleanProps = defineRule({
32392
33278
  reportIfMany(booleanLikePropNames, componentName, reportNode);
32393
33279
  return;
32394
33280
  }
32395
- if (isNodeOfType(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
33281
+ if (isNodeOfType(propsBinding, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, propsBinding.name)], componentName, reportNode);
32396
33282
  };
32397
33283
  return {
32398
33284
  FunctionDeclaration(node) {
@@ -32514,7 +33400,7 @@ const noMirrorPropEffect = defineRule({
32514
33400
  if (mirrorBindings.length === 0) return;
32515
33401
  for (const statement of componentBody.body ?? []) {
32516
33402
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
32517
- const effectCall = statement.expression;
33403
+ const effectCall = unwrapDiscardedExpression(statement);
32518
33404
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
32519
33405
  if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
32520
33406
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
@@ -32528,7 +33414,7 @@ const noMirrorPropEffect = defineRule({
32528
33414
  const bodyStatements = getCallbackStatements(callback);
32529
33415
  if (bodyStatements.length !== 1) continue;
32530
33416
  const onlyStatement = bodyStatements[0];
32531
- const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
33417
+ const expression = unwrapDiscardedExpression(onlyStatement);
32532
33418
  if (!isNodeOfType(expression, "CallExpression")) continue;
32533
33419
  if (!isNodeOfType(expression.callee, "Identifier")) continue;
32534
33420
  if (!isSetterIdentifier(expression.callee.name)) continue;
@@ -32573,25 +33459,60 @@ const resolveSettings$16 = (settings) => {
32573
33459
  };
32574
33460
  const isHocCall = (call, scopes) => {
32575
33461
  if (!isNodeOfType(call, "CallExpression")) return false;
32576
- const calleeName = flattenCalleeName(call.callee);
32577
- if (calleeName && REACT_HOC_NAMES.has(calleeName)) return true;
33462
+ if (isReactApiCall(call, REACT_HOC_NAMES, scopes, {
33463
+ allowGlobalReactNamespace: true,
33464
+ allowUnboundBareCalls: true
33465
+ })) return true;
33466
+ if (isReactHocMemberReference(call.callee, scopes)) return true;
32578
33467
  if (!isNodeOfType(call.callee, "Identifier")) return false;
32579
33468
  const symbol = scopes.symbolFor(call.callee);
32580
33469
  if (!symbol) return false;
32581
- return symbolMapsToHoc(symbol);
33470
+ return symbolMapsToHoc(symbol, scopes, /* @__PURE__ */ new Set());
33471
+ };
33472
+ const isReactImportEquals = (symbol) => {
33473
+ if (symbol.kind !== "ts-import-equals" || !isNodeOfType(symbol.declarationNode, "TSImportEqualsDeclaration")) return false;
33474
+ const moduleReference = symbol.declarationNode.moduleReference;
33475
+ return Boolean(isNodeOfType(moduleReference, "TSExternalModuleReference") && isNodeOfType(moduleReference.expression, "Literal") && typeof moduleReference.expression.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(moduleReference.expression.value));
32582
33476
  };
32583
- const symbolMapsToHoc = (symbol) => {
32584
- if (REACT_HOC_NAMES.has(symbol.name)) {
32585
- if (symbol.kind === "import") return true;
33477
+ const isRequireReactCall$1 = (node, scopes) => {
33478
+ if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "Identifier") || node.callee.name !== "require" || !scopes.isGlobalReference(node.callee)) return false;
33479
+ const moduleSpecifier = node.arguments[0];
33480
+ return Boolean(moduleSpecifier && isNodeOfType(moduleSpecifier, "Literal") && typeof moduleSpecifier.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(moduleSpecifier.value));
33481
+ };
33482
+ const isReactNamespaceExpression = (node, scopes) => {
33483
+ if (isRequireReactCall$1(node, scopes)) return true;
33484
+ if (!isNodeOfType(node, "Identifier")) return false;
33485
+ const symbol = scopes.symbolFor(node);
33486
+ if (!symbol) return node.name === "React" && scopes.isGlobalReference(node);
33487
+ if (symbol.initializer && isRequireReactCall$1(symbol.initializer, scopes)) return true;
33488
+ if (isReactImportEquals(symbol)) return true;
33489
+ return isReactNamespaceImport(node, scopes);
33490
+ };
33491
+ const isReactHocMemberReference = (node, scopes) => Boolean(isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.property, "Identifier") && REACT_HOC_NAMES.has(node.property.name) && isReactNamespaceExpression(node.object, scopes));
33492
+ const getDestructuredPropertyName$1 = (symbol) => {
33493
+ const property = symbol.bindingIdentifier.parent;
33494
+ if (!property || !isNodeOfType(property, "Property") || !property.parent || !isNodeOfType(property.parent, "ObjectPattern")) return null;
33495
+ if (isNodeOfType(property.key, "Identifier") && !property.computed) return property.key.name;
33496
+ if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") return property.key.value;
33497
+ return null;
33498
+ };
33499
+ const symbolMapsToHoc = (symbol, scopes, visitedSymbolIds) => {
33500
+ if (visitedSymbolIds.has(symbol.id)) return false;
33501
+ visitedSymbolIds.add(symbol.id);
33502
+ if (symbol.kind === "import") {
33503
+ const importedName = getImportedName(symbol.declarationNode);
33504
+ return Boolean(isImportedFromReact(symbol) && importedName && REACT_HOC_NAMES.has(importedName));
32586
33505
  }
32587
33506
  const init = symbol.initializer;
32588
33507
  if (!init) return false;
32589
- if (isNodeOfType(init, "MemberExpression")) {
32590
- const flat = flattenCalleeName(init);
32591
- if (flat && REACT_HOC_NAMES.has(flat)) return true;
33508
+ const destructuredPropertyName = getDestructuredPropertyName$1(symbol);
33509
+ if (destructuredPropertyName && REACT_HOC_NAMES.has(destructuredPropertyName) && isReactNamespaceExpression(init, scopes)) return true;
33510
+ if (isReactHocMemberReference(init, scopes)) return true;
33511
+ if (isNodeOfType(init, "Identifier")) {
33512
+ const initializedFromSymbol = scopes.symbolFor(init);
33513
+ if (initializedFromSymbol) return symbolMapsToHoc(initializedFromSymbol, scopes, visitedSymbolIds);
33514
+ return REACT_HOC_NAMES.has(init.name) && scopes.isGlobalReference(init);
32592
33515
  }
32593
- if (isNodeOfType(init, "Identifier") && REACT_HOC_NAMES.has(init.name)) return true;
32594
- if (REACT_HOC_NAMES.has(symbol.name) && isNodeOfType(init, "Identifier") && init.name === "React") return true;
32595
33516
  return false;
32596
33517
  };
32597
33518
  const isTrivialPassthroughChild = (child) => {
@@ -32649,26 +33570,14 @@ const isHocComponent = (call, scopes) => {
32649
33570
  };
32650
33571
  const containsJsx = (root) => {
32651
33572
  let found = false;
32652
- const visit = (node) => {
32653
- if (found) return;
33573
+ walkAst(root, (node) => {
33574
+ if (found) return false;
32654
33575
  if (node.type === "JSXElement" || node.type === "JSXFragment") {
32655
33576
  found = true;
32656
- return;
32657
- }
32658
- if (node !== root) {
32659
- if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "ClassDeclaration" || node.type === "ClassExpression") return;
32660
- }
32661
- const record = node;
32662
- for (const key of Object.keys(record)) {
32663
- if (key === "parent") continue;
32664
- const child = record[key];
32665
- if (Array.isArray(child)) {
32666
- for (const item of child) if (isAstNode(item)) visit(item);
32667
- } else if (isAstNode(child)) visit(child);
32668
- if (found) return;
33577
+ return false;
32669
33578
  }
32670
- };
32671
- visit(root);
33579
+ if (node !== root && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "ClassDeclaration" || node.type === "ClassExpression")) return false;
33580
+ });
32672
33581
  return found;
32673
33582
  };
32674
33583
  const expressionContainsJsx = (expression) => {
@@ -32692,7 +33601,7 @@ const unwrapTsCast = (expression) => {
32692
33601
  while (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression") current = stripParenExpression(current.expression);
32693
33602
  return current;
32694
33603
  };
32695
- const collectReExportedNames = (program) => {
33604
+ const collectReExportedNames = (program, scopes) => {
32696
33605
  const names = /* @__PURE__ */ new Set();
32697
33606
  if (!isNodeOfType(program, "Program")) return names;
32698
33607
  for (const statement of program.body) {
@@ -32716,8 +33625,7 @@ const collectReExportedNames = (program) => {
32716
33625
  if (!declarator.init) continue;
32717
33626
  const init = unwrapTsCast(declarator.init);
32718
33627
  if (isNodeOfType(init, "CallExpression")) {
32719
- const calleeName = flattenCalleeName(init.callee);
32720
- if (calleeName && REACT_HOC_NAMES.has(calleeName)) {
33628
+ if (isHocCall(init, scopes)) {
32721
33629
  const wrappedArg = init.arguments[0];
32722
33630
  if (wrappedArg && isNodeOfType(wrappedArg, "Identifier")) names.add(wrappedArg.name);
32723
33631
  }
@@ -32786,16 +33694,7 @@ const recordComponent = (context, name, reportNode, isStateless) => {
32786
33694
  isStateless
32787
33695
  });
32788
33696
  };
32789
- const walkChildren = (node, context) => {
32790
- const record = node;
32791
- for (const key of Object.keys(record)) {
32792
- if (key === "parent") continue;
32793
- const child = record[key];
32794
- if (Array.isArray(child)) {
32795
- for (const item of child) if (isAstNode(item)) walkComponentSearch(item, context);
32796
- } else if (isAstNode(child)) walkComponentSearch(child, context);
32797
- }
32798
- };
33697
+ const walkChildren = (node, context) => forEachChildNode(node, context.visitChild);
32799
33698
  const walkComponentSearch = (node, context) => {
32800
33699
  if (isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression")) {
32801
33700
  if (isEs6Component(node)) {
@@ -32897,12 +33796,13 @@ const noMultiComp = defineRule({
32897
33796
  components: [],
32898
33797
  componentDepth: 0,
32899
33798
  currentVarName: null,
32900
- scopes: context.scopes
33799
+ scopes: context.scopes,
33800
+ visitChild: (child) => walkComponentSearch(child, visitContext)
32901
33801
  };
32902
33802
  for (const statement of node.body) walkComponentSearch(statement, visitContext);
32903
33803
  const flagged = settings.ignoreStateless ? visitContext.components.filter((component) => !component.isStateless) : visitContext.components;
32904
33804
  if (flagged.length <= 2) return;
32905
- const reExportedNames = collectReExportedNames(node);
33805
+ const reExportedNames = collectReExportedNames(node, context.scopes);
32906
33806
  const exportedCount = flagged.filter((component) => isExportedDeclaration(component.reportNode, reExportedNames)).length;
32907
33807
  if (exportedCount >= flagged.length || flagged.length >= 4 && exportedCount >= Math.floor(flagged.length * .7) || flagged.length >= 8 && exportedCount >= Math.floor(flagged.length * .5)) return;
32908
33808
  const isSmallFeatureModule = exportedCount > 0 && exportedCount <= 2 && exportedCount < flagged.length;
@@ -34294,19 +35194,6 @@ const DATA_SINK_METHOD_NAMES = new Set([
34294
35194
  "deserialize"
34295
35195
  ]);
34296
35196
  //#endregion
34297
- //#region src/plugin/utils/get-call-method-name.ts
34298
- /**
34299
- * Returns the static method name of a call's callee when it's a
34300
- * non-computed MemberExpression (`obj.method` → `"method"`), or
34301
- * `null` otherwise. Used by `no-pass-data-to-parent` and
34302
- * `no-pass-live-state-to-parent` to match the receiver-bound
34303
- * method-call shape against an allow/block list.
34304
- */
34305
- const getCallMethodName = (callee) => {
34306
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
34307
- return null;
34308
- };
34309
- //#endregion
34310
35197
  //#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
34311
35198
  const isUseStateIdentifier = (identifier) => {
34312
35199
  if (!isNodeOfType(identifier, "Identifier")) return false;
@@ -34551,6 +35438,14 @@ const isHandlerBagArgument = (analysis, argument) => {
34551
35438
  };
34552
35439
  const getFunctionalUpdaterDataRefs = (analysis, updater) => getDownstreamRefs(analysis, updater).filter((updaterRef) => !updaterRef.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updater));
34553
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
+ ]);
34554
35449
  const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
34555
35450
  const node = def.node;
34556
35451
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
@@ -34573,6 +35468,19 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
34573
35468
  if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
34574
35469
  return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isProp(analysis, downstreamRef)));
34575
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
+ };
34576
35484
  const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
34577
35485
  const isCalleePosition = (identifier) => {
34578
35486
  const parent = identifier.parent;
@@ -34634,10 +35542,11 @@ const noPassDataToParent = defineRule({
34634
35542
  if (argumentRef && resolveToFunction(argumentRef)) return [];
34635
35543
  }
34636
35544
  return getDownstreamRefs(analysis, argument);
34637
- }).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
35545
+ }).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
34638
35546
  if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
34639
35547
  if (!argsUpstreamRefs.some((argRef) => {
34640
35548
  if (isUseStateIdentifier(argRef.identifier)) return false;
35549
+ if (isExternalSubscriptionHookRef(argRef)) return false;
34641
35550
  if (isProp(analysis, argRef)) return false;
34642
35551
  if (isUseRefIdentifier(argRef.identifier)) return false;
34643
35552
  if (isRefCurrent(argRef)) return false;
@@ -34932,6 +35841,30 @@ const guardsRenderShape = (comparison) => {
34932
35841
  }
34933
35842
  return false;
34934
35843
  };
35844
+ const isPropsChildrenLength = (node, scopes) => {
35845
+ const unwrappedNode = stripParenExpression(node);
35846
+ return isNodeOfType(unwrappedNode, "MemberExpression") && !unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Identifier") && unwrappedNode.property.name === "length" && resolvesToPropsChildren(stripParenExpression(unwrappedNode.object), scopes);
35847
+ };
35848
+ const isLargeTextLengthComparison = (node, scopes) => {
35849
+ const unwrappedNode = stripParenExpression(node);
35850
+ if (!isNodeOfType(unwrappedNode, "BinaryExpression")) return false;
35851
+ const leftIsLength = isPropsChildrenLength(unwrappedNode.left, scopes);
35852
+ const rightIsLength = isPropsChildrenLength(unwrappedNode.right, scopes);
35853
+ const thresholdNode = leftIsLength ? unwrappedNode.right : unwrappedNode.left;
35854
+ if (!leftIsLength && !rightIsLength || !isNodeOfType(thresholdNode, "Literal")) return false;
35855
+ if (typeof thresholdNode.value !== "number" || thresholdNode.value < 1e3) return false;
35856
+ return leftIsLength ? unwrappedNode.operator === ">" || unwrappedNode.operator === ">=" : unwrappedNode.operator === "<" || unwrappedNode.operator === "<=";
35857
+ };
35858
+ const isLargeStringOptimizationGuard = (comparison, scopes) => {
35859
+ let current = findTransparentExpressionRoot(comparison);
35860
+ while (current.parent) {
35861
+ const parent = current.parent;
35862
+ if (!isNodeOfType(parent, "LogicalExpression") || parent.operator !== "&&") return false;
35863
+ if (isLargeTextLengthComparison(parent.left === current ? parent.right : parent.left, scopes)) return true;
35864
+ current = findTransparentExpressionRoot(parent);
35865
+ }
35866
+ return false;
35867
+ };
34935
35868
  const noPolymorphicChildren = defineRule({
34936
35869
  id: "no-polymorphic-children",
34937
35870
  title: "Children type checked at runtime",
@@ -34945,6 +35878,7 @@ const noPolymorphicChildren = defineRule({
34945
35878
  const isStringLiteral = (operand) => isNodeOfType(operand, "Literal") && operand.value === "string";
34946
35879
  if (!isStringLiteral(node.left) && !isStringLiteral(node.right)) return;
34947
35880
  if (!guardsRenderShape(node)) return;
35881
+ if (isLargeStringOptimizationGuard(node, context.scopes)) return;
34948
35882
  context.report({
34949
35883
  node,
34950
35884
  message: "Your users hit inconsistent behavior because `typeof children === \"string\"` makes this component switch on what callers pass, so add clear subcomponents like `<Button.Text>` instead."
@@ -35096,7 +36030,7 @@ const noPreventDefault = defineRule({
35096
36030
  const isServerActionsFramework = hasCapability(context.settings, "server-actions");
35097
36031
  const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
35098
36032
  return { JSXOpeningElement(node) {
35099
- const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
36033
+ const elementName = resolveJsxElementType(node);
35100
36034
  if (!elementName) return;
35101
36035
  const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
35102
36036
  if (!targetEventProps) return;
@@ -35162,6 +36096,27 @@ const hasPreviousValueDep = (effectNode, depElements) => {
35162
36096
  }
35163
36097
  return false;
35164
36098
  };
36099
+ const isStateLikeDependency = (analysis, element, isPropName) => {
36100
+ if (!isNodeOfType(element, "Identifier") || isPropName(element.name)) return false;
36101
+ if (!analysis) return true;
36102
+ const reference = getRef(analysis, element);
36103
+ if (!reference) return true;
36104
+ const upstreamReferences = getUpstreamRefs(analysis, reference);
36105
+ if (upstreamReferences.some((upstreamReference) => isState(analysis, upstreamReference))) return true;
36106
+ return !upstreamReferences.some((upstreamReference) => isProp(analysis, upstreamReference));
36107
+ };
36108
+ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
36109
+ const callee = stripParenExpression(callExpression.callee);
36110
+ if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "current") return null;
36111
+ const receiver = stripParenExpression(callee.object);
36112
+ if (!isNodeOfType(receiver, "Identifier")) return null;
36113
+ const binding = findVariableInitializer(callExpression, receiver.name);
36114
+ if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) return null;
36115
+ if (getCalleeName$2(binding.initializer) !== "useRef") return null;
36116
+ const callbackArgument = binding.initializer.arguments?.[0];
36117
+ if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
36118
+ return isPropName(callbackArgument.name) ? callbackArgument.name : null;
36119
+ };
35165
36120
  const noPropCallbackInEffect = defineRule({
35166
36121
  id: "no-prop-callback-in-effect",
35167
36122
  title: "Parent kept in sync with a callback effect",
@@ -35178,11 +36133,11 @@ const noPropCallbackInEffect = defineRule({
35178
36133
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
35179
36134
  const depsNode = node.arguments[1];
35180
36135
  if (!isNodeOfType(depsNode, "ArrayExpression") || !depsNode.elements?.length) return;
35181
- const stateLikeDeps = (depsNode.elements ?? []).filter((element) => isNodeOfType(element, "Identifier") && !propStackTracker.isPropName(element.name));
36136
+ const analysis = getProgramAnalysis(node);
36137
+ const stateLikeDeps = (depsNode.elements ?? []).filter((element) => element && isStateLikeDependency(analysis, element, propStackTracker.isPropName));
35182
36138
  if (stateLikeDeps.length === 0) return;
35183
36139
  if (isRefLatchGuardedEffect(callback.body)) return;
35184
36140
  if (hasPreviousValueDep(node, depsNode.elements ?? [])) return;
35185
- const analysis = getProgramAnalysis(node);
35186
36141
  if (analysis) {
35187
36142
  const stateLikeDepRefs = [];
35188
36143
  for (const element of stateLikeDeps) {
@@ -35194,9 +36149,9 @@ const noPropCallbackInEffect = defineRule({
35194
36149
  const reportedNodes = /* @__PURE__ */ new Set();
35195
36150
  walkInsideStatementBlocks(callback.body, (child) => {
35196
36151
  if (!isNodeOfType(child, "CallExpression")) return;
35197
- if (!isNodeOfType(child.callee, "Identifier")) return;
35198
- const calleeName = child.callee.name;
35199
- if (!propStackTracker.isPropName(calleeName)) return;
36152
+ const directCallee = stripParenExpression(child.callee);
36153
+ const calleeName = isNodeOfType(directCallee, "Identifier") && propStackTracker.isPropName(directCallee.name) && directCallee.name || getRefHeldPropCallbackName(child, propStackTracker.isPropName);
36154
+ if (!calleeName) return;
35200
36155
  if (!isResultDiscardedCall(child)) return;
35201
36156
  if (reportedNodes.has(child)) return;
35202
36157
  reportedNodes.add(child);
@@ -36560,7 +37515,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
36560
37515
  return expressionReadsStateValue(argument, stateName);
36561
37516
  };
36562
37517
  const getUnconditionalSetterCall = (statement, setterNames) => {
36563
- const expression = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
37518
+ const expression = unwrapDiscardedExpression(statement);
36564
37519
  if (!isNodeOfType(expression, "CallExpression")) return null;
36565
37520
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
36566
37521
  if (!setterNames.has(expression.callee.name)) return null;
@@ -36909,7 +37864,7 @@ const noSelfUpdatingEffect = defineRule({
36909
37864
  const setterNames = new Set(setterNameToStateName.keys());
36910
37865
  for (const statement of functionBody.body ?? []) {
36911
37866
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
36912
- const effectCall = statement.expression;
37867
+ const effectCall = unwrapDiscardedExpression(statement);
36913
37868
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
36914
37869
  if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
36915
37870
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
@@ -37539,9 +38494,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
37539
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.",
37540
38495
  create: (context) => ({ JSXOpeningElement(node) {
37541
38496
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
37542
- const firstCharacter = node.name.name.charCodeAt(0);
38497
+ const elementName = resolveJsxElementType(node);
38498
+ const firstCharacter = elementName.charCodeAt(0);
37543
38499
  if (firstCharacter < 97 || firstCharacter > 122) return;
37544
- if (node.name.name.includes("-")) return;
38500
+ if (elementName.includes("-")) return;
37545
38501
  for (const attribute of node.attributes) {
37546
38502
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
37547
38503
  if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
@@ -37929,8 +38885,7 @@ const noUncontrolledInput = defineRule({
37929
38885
  const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
37930
38886
  walkAst(componentBody, (child) => {
37931
38887
  if (!isNodeOfType(child, "JSXOpeningElement")) return;
37932
- if (!isNodeOfType(child.name, "JSXIdentifier")) return;
37933
- const tagName = child.name.name;
38888
+ const tagName = resolveJsxElementType(child);
37934
38889
  if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
37935
38890
  const attributes = child.attributes ?? [];
37936
38891
  if (hasJsxSpreadAttribute(attributes)) return;
@@ -37991,7 +38946,7 @@ const noUndeferredThirdParty = defineRule({
37991
38946
  severity: "warn",
37992
38947
  recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
37993
38948
  create: (context) => ({ JSXOpeningElement(node) {
37994
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
38949
+ if (resolveJsxElementType(node) !== "script") return;
37995
38950
  const attributes = node.attributes ?? [];
37996
38951
  const srcAttribute = findJsxAttribute(attributes, "src");
37997
38952
  if (!srcAttribute) return;
@@ -39206,7 +40161,7 @@ const noUnknownProperty = defineRule({
39206
40161
  }
39207
40162
  if (fileIsNonReactJsx) return;
39208
40163
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
39209
- const elementType = node.name.name;
40164
+ const elementType = resolveJsxElementType(node);
39210
40165
  const firstCharacter = elementType.charCodeAt(0);
39211
40166
  if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
39212
40167
  let isValidHtmlTag = isKnownDomTag(elementType);
@@ -39370,50 +40325,35 @@ const resolveSettings$8 = (settings) => {
39370
40325
  };
39371
40326
  const expressionContainsJsxOrCreateElement = (root) => {
39372
40327
  let found = false;
39373
- const visit = (node) => {
39374
- if (found) return;
40328
+ walkAst(root, (node) => {
40329
+ if (found) return false;
40330
+ if (node !== root && NESTED_FUNCTION_TYPES.has(node.type)) return false;
39375
40331
  if (node.type === "JSXElement" || node.type === "JSXFragment") {
39376
40332
  found = true;
39377
- return;
40333
+ return false;
39378
40334
  }
39379
40335
  if (isNodeOfType(node, "CallExpression") && isCreateElementCall(node)) {
39380
40336
  found = true;
39381
- return;
39382
- }
39383
- const record = node;
39384
- for (const key of Object.keys(record)) {
39385
- if (key === "parent") continue;
39386
- const child = record[key];
39387
- if (Array.isArray(child)) for (const item of child) {
39388
- if (!isAstNode(item)) continue;
39389
- if (NESTED_FUNCTION_TYPES.has(item.type)) continue;
39390
- visit(item);
39391
- if (found) return;
39392
- }
39393
- else if (isAstNode(child)) {
39394
- if (NESTED_FUNCTION_TYPES.has(child.type)) continue;
39395
- visit(child);
39396
- }
39397
- if (found) return;
40337
+ return false;
39398
40338
  }
39399
- };
39400
- visit(root);
40339
+ });
39401
40340
  return found;
39402
40341
  };
40342
+ const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
39403
40343
  const isReactClassComponent = (classNode) => {
39404
40344
  if (isEs6Component(classNode)) return true;
39405
40345
  return expressionContainsJsxOrCreateElement(classNode);
39406
40346
  };
39407
- const findEnclosingComponent = (node) => {
40347
+ const findEnclosingComponent = (node, scopes) => {
39408
40348
  let walker = node.parent;
39409
40349
  while (walker) {
39410
40350
  if (isFunctionLike$1(walker)) {
39411
40351
  const componentName = inferFunctionLikeName(walker);
39412
- if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
40352
+ if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
39413
40353
  component: walker,
39414
40354
  name: componentName
39415
40355
  };
39416
- if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
40356
+ if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
39417
40357
  component: walker,
39418
40358
  name: null
39419
40359
  };
@@ -39652,7 +40592,7 @@ const noUnstableNestedComponents = defineRule({
39652
40592
  if (renderPropRegex.test(propInfo.propName)) return;
39653
40593
  if (settings.allowAsProps) return;
39654
40594
  }
39655
- const enclosing = findEnclosingComponent(candidateNode);
40595
+ const enclosing = findEnclosingComponent(candidateNode, context.scopes);
39656
40596
  if (!enclosing) return;
39657
40597
  const gatedName = propInfo ? null : requiredInstantiationName;
39658
40598
  queuedReports.push({
@@ -39663,7 +40603,7 @@ const noUnstableNestedComponents = defineRule({
39663
40603
  });
39664
40604
  };
39665
40605
  const checkFunctionLike = (node) => {
39666
- if (!expressionContainsJsxOrCreateElement(node)) return;
40606
+ if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
39667
40607
  const inferredName = inferFunctionLikeName(node);
39668
40608
  const propInfo = isComponentDeclaredInProp(node);
39669
40609
  const isObjectCallback = isObjectCallbackCandidate(node);
@@ -40323,19 +41263,6 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
40323
41263
  reportNode
40324
41264
  };
40325
41265
  };
40326
- const collectRelevantNodes = (programRoot) => {
40327
- const exportNodes = [];
40328
- const componentCandidates = [];
40329
- walkAst(programRoot, (child) => {
40330
- const childType = child.type;
40331
- if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
40332
- else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
40333
- });
40334
- return {
40335
- exportNodes,
40336
- componentCandidates
40337
- };
40338
- };
40339
41266
  const isEntryPointFile = (filename) => {
40340
41267
  const lastSlash = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"));
40341
41268
  const basename = lastSlash === -1 ? filename : filename.slice(lastSlash + 1);
@@ -40374,197 +41301,212 @@ const onlyExportComponents = defineRule({
40374
41301
  category: "Architecture",
40375
41302
  create: (context) => {
40376
41303
  const settings = resolveSettings$6(context.settings);
40377
- return { Program(node) {
40378
- if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
40379
- const { exportNodes, componentCandidates } = collectRelevantNodes(node);
40380
- const localComponentNames = /* @__PURE__ */ new Set();
40381
- const state = {
40382
- customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
40383
- allowExportNames: new Set(settings.allowExportNames),
40384
- allowConstantExport: settings.allowConstantExport,
40385
- localComponentNames,
40386
- scopes: context.scopes
40387
- };
40388
- for (const child of componentCandidates) {
40389
- if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
40390
- if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
40391
- }
40392
- if (isNodeOfType(child, "ClassDeclaration") && child.id) {
40393
- if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
40394
- }
40395
- if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
40396
- const initializer = child.init;
40397
- if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
40398
- const expression = initializer ? skipTsExpression(initializer) : null;
40399
- if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
41304
+ if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return {};
41305
+ const exportNodes = [];
41306
+ const componentCandidates = [];
41307
+ const pushExportNode = (node) => {
41308
+ exportNodes.push(node);
41309
+ };
41310
+ const pushComponentCandidate = (node) => {
41311
+ componentCandidates.push(node);
41312
+ };
41313
+ return {
41314
+ ExportAllDeclaration: pushExportNode,
41315
+ ExportDefaultDeclaration: pushExportNode,
41316
+ ExportNamedDeclaration: pushExportNode,
41317
+ FunctionDeclaration: pushComponentCandidate,
41318
+ VariableDeclarator: pushComponentCandidate,
41319
+ ClassDeclaration: pushComponentCandidate,
41320
+ "Program:exit"() {
41321
+ const localComponentNames = /* @__PURE__ */ new Set();
41322
+ const state = {
41323
+ customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
41324
+ allowExportNames: new Set(settings.allowExportNames),
41325
+ allowConstantExport: settings.allowConstantExport,
41326
+ localComponentNames,
41327
+ scopes: context.scopes
41328
+ };
41329
+ for (const child of componentCandidates) {
41330
+ if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
41331
+ if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
41332
+ }
41333
+ if (isNodeOfType(child, "ClassDeclaration") && child.id) {
41334
+ if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
41335
+ }
41336
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
41337
+ const initializer = child.init;
41338
+ if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
41339
+ const expression = initializer ? skipTsExpression(initializer) : null;
41340
+ if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
41341
+ }
40400
41342
  }
40401
41343
  }
40402
- }
40403
- const exports = [];
40404
- let hasReactExport = false;
40405
- let hasAnyExports = false;
40406
- const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
40407
- for (const child of exportNodes) {
40408
- if (isNodeOfType(child, "ExportAllDeclaration")) {
40409
- if (child.exportKind === "type") continue;
40410
- hasAnyExports = true;
40411
- context.report({
40412
- node: child,
40413
- message: EXPORT_ALL_MESSAGE
40414
- });
40415
- continue;
40416
- }
40417
- if (isNodeOfType(child, "ExportDefaultDeclaration")) {
40418
- hasAnyExports = true;
40419
- const declaration = child.declaration;
40420
- const stripped = skipTsExpression(declaration);
40421
- if (isNodeOfType(stripped, "FunctionDeclaration") || isNodeOfType(stripped, "FunctionExpression")) {
40422
- if (stripped.id) {
40423
- const idNode = stripped.id;
40424
- isExportedNodeIds.add(stripped);
40425
- exports.push(classifyExport(idNode.name, idNode, true, null, state));
40426
- } else {
40427
- context.report({
41344
+ const exports = [];
41345
+ let hasReactExport = false;
41346
+ let hasAnyExports = false;
41347
+ const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
41348
+ for (const child of exportNodes) {
41349
+ if (isNodeOfType(child, "ExportAllDeclaration")) {
41350
+ if (child.exportKind === "type") continue;
41351
+ hasAnyExports = true;
41352
+ context.report({
41353
+ node: child,
41354
+ message: EXPORT_ALL_MESSAGE
41355
+ });
41356
+ continue;
41357
+ }
41358
+ if (isNodeOfType(child, "ExportDefaultDeclaration")) {
41359
+ hasAnyExports = true;
41360
+ const declaration = child.declaration;
41361
+ const stripped = skipTsExpression(declaration);
41362
+ if (isNodeOfType(stripped, "FunctionDeclaration") || isNodeOfType(stripped, "FunctionExpression")) {
41363
+ if (stripped.id) {
41364
+ const idNode = stripped.id;
41365
+ isExportedNodeIds.add(stripped);
41366
+ exports.push(classifyExport(idNode.name, idNode, true, null, state));
41367
+ } else {
41368
+ context.report({
41369
+ node: stripped,
41370
+ message: ANONYMOUS_MESSAGE
41371
+ });
41372
+ hasReactExport = true;
41373
+ }
41374
+ continue;
41375
+ }
41376
+ if (isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "ClassExpression")) {
41377
+ if (stripped.id) {
41378
+ const idNode = stripped.id;
41379
+ isExportedNodeIds.add(stripped);
41380
+ if (isReactComponentName(idNode.name) && isEs6Component(stripped)) hasReactExport = true;
41381
+ else exports.push({
41382
+ kind: "non-component",
41383
+ reportNode: idNode
41384
+ });
41385
+ } else context.report({
40428
41386
  node: stripped,
40429
41387
  message: ANONYMOUS_MESSAGE
40430
41388
  });
40431
- hasReactExport = true;
41389
+ continue;
40432
41390
  }
40433
- continue;
40434
- }
40435
- if (isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "ClassExpression")) {
40436
- if (stripped.id) {
40437
- const idNode = stripped.id;
40438
- isExportedNodeIds.add(stripped);
40439
- if (isReactComponentName(idNode.name) && isEs6Component(stripped)) hasReactExport = true;
40440
- else exports.push({
41391
+ if (isNodeOfType(stripped, "Identifier")) {
41392
+ exports.push(classifyExport(stripped.name, stripped, false, null, state));
41393
+ continue;
41394
+ }
41395
+ if (isNodeOfType(stripped, "CallExpression")) {
41396
+ if (isRouteFactoryCall(stripped)) {
41397
+ hasReactExport = true;
41398
+ continue;
41399
+ }
41400
+ const isHoc = isHocCallee(stripped.callee, state);
41401
+ const firstArg = stripped.arguments[0];
41402
+ const firstArgIsValid = Boolean(firstArg) && (() => {
41403
+ if (!firstArg) return false;
41404
+ const expression = skipTsExpression(firstArg);
41405
+ if (isNodeOfType(expression, "Identifier")) return true;
41406
+ if (isNodeOfType(expression, "FunctionExpression") && expression.id) return true;
41407
+ if (isNodeOfType(expression, "CallExpression") && isHocCallee(expression.callee, state)) return true;
41408
+ return false;
41409
+ })();
41410
+ if (isHoc && firstArgIsValid) hasReactExport = true;
41411
+ else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
40441
41412
  kind: "non-component",
40442
- reportNode: idNode
41413
+ reportNode: stripped
41414
+ });
41415
+ else context.report({
41416
+ node: stripped,
41417
+ message: ANONYMOUS_MESSAGE
41418
+ });
41419
+ continue;
41420
+ }
41421
+ if (isNodeOfType(stripped, "ObjectExpression")) {
41422
+ context.report({
41423
+ node: stripped,
41424
+ message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
41425
+ });
41426
+ continue;
41427
+ }
41428
+ if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
41429
+ context.report({
41430
+ node: stripped,
41431
+ message: ANONYMOUS_MESSAGE
40443
41432
  });
40444
- } else context.report({
40445
- node: stripped,
40446
- message: ANONYMOUS_MESSAGE
40447
- });
40448
- continue;
40449
- }
40450
- if (isNodeOfType(stripped, "Identifier")) {
40451
- exports.push(classifyExport(stripped.name, stripped, false, null, state));
40452
- continue;
40453
- }
40454
- if (isNodeOfType(stripped, "CallExpression")) {
40455
- if (isRouteFactoryCall(stripped)) {
40456
- hasReactExport = true;
40457
41433
  continue;
40458
41434
  }
40459
- const isHoc = isHocCallee(stripped.callee, state);
40460
- const firstArg = stripped.arguments[0];
40461
- const firstArgIsValid = Boolean(firstArg) && (() => {
40462
- if (!firstArg) return false;
40463
- const expression = skipTsExpression(firstArg);
40464
- if (isNodeOfType(expression, "Identifier")) return true;
40465
- if (isNodeOfType(expression, "FunctionExpression") && expression.id) return true;
40466
- if (isNodeOfType(expression, "CallExpression") && isHocCallee(expression.callee, state)) return true;
40467
- return false;
40468
- })();
40469
- if (isHoc && firstArgIsValid) hasReactExport = true;
40470
- else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
40471
- kind: "non-component",
40472
- reportNode: stripped
40473
- });
40474
- else context.report({
40475
- node: stripped,
40476
- message: ANONYMOUS_MESSAGE
40477
- });
40478
- continue;
40479
- }
40480
- if (isNodeOfType(stripped, "ObjectExpression")) {
40481
- context.report({
40482
- node: stripped,
40483
- message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
40484
- });
40485
- continue;
40486
- }
40487
- if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
40488
41435
  context.report({
40489
- node: stripped,
41436
+ node: child,
40490
41437
  message: ANONYMOUS_MESSAGE
40491
41438
  });
40492
41439
  continue;
40493
41440
  }
40494
- context.report({
40495
- node: child,
40496
- message: ANONYMOUS_MESSAGE
40497
- });
40498
- continue;
40499
- }
40500
- if (isNodeOfType(child, "ExportNamedDeclaration")) {
40501
- if (child.exportKind === "type") continue;
40502
- hasAnyExports = true;
40503
- if (child.declaration) {
40504
- const declaration = child.declaration;
40505
- if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
40506
- isExportedNodeIds.add(declaration);
40507
- exports.push(classifyExport(declaration.id.name, declaration.id, true, null, state));
40508
- } else if (isNodeOfType(declaration, "ClassDeclaration") && declaration.id) {
40509
- isExportedNodeIds.add(declaration);
40510
- if (isReactComponentName(declaration.id.name) && isEs6Component(declaration)) exports.push({ kind: "react-component" });
40511
- else exports.push({
40512
- kind: "non-component",
40513
- reportNode: declaration.id
40514
- });
40515
- } else if (isNodeOfType(declaration, "VariableDeclaration")) for (const declarator of declaration.declarations) {
40516
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
40517
- isExportedNodeIds.add(declarator);
40518
- const isFunction = canBeReactFunctionComponent(declarator.init ?? null, state);
40519
- exports.push(classifyExport(declarator.id.name, declarator.id, isFunction, declarator.init, state));
40520
- }
40521
- else if (declaration.type === "TSEnumDeclaration" || declaration.type === "TSInterfaceDeclaration" || declaration.type === "TSTypeAliasDeclaration") {
40522
- if (declaration.type === "TSEnumDeclaration") exports.push({
40523
- kind: "non-component",
40524
- reportNode: declaration
40525
- });
41441
+ if (isNodeOfType(child, "ExportNamedDeclaration")) {
41442
+ if (child.exportKind === "type") continue;
41443
+ hasAnyExports = true;
41444
+ if (child.declaration) {
41445
+ const declaration = child.declaration;
41446
+ if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
41447
+ isExportedNodeIds.add(declaration);
41448
+ exports.push(classifyExport(declaration.id.name, declaration.id, true, null, state));
41449
+ } else if (isNodeOfType(declaration, "ClassDeclaration") && declaration.id) {
41450
+ isExportedNodeIds.add(declaration);
41451
+ if (isReactComponentName(declaration.id.name) && isEs6Component(declaration)) exports.push({ kind: "react-component" });
41452
+ else exports.push({
41453
+ kind: "non-component",
41454
+ reportNode: declaration.id
41455
+ });
41456
+ } else if (isNodeOfType(declaration, "VariableDeclaration")) for (const declarator of declaration.declarations) {
41457
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
41458
+ isExportedNodeIds.add(declarator);
41459
+ const isFunction = canBeReactFunctionComponent(declarator.init ?? null, state);
41460
+ exports.push(classifyExport(declarator.id.name, declarator.id, isFunction, declarator.init, state));
41461
+ }
41462
+ else if (declaration.type === "TSEnumDeclaration" || declaration.type === "TSInterfaceDeclaration" || declaration.type === "TSTypeAliasDeclaration") {
41463
+ if (declaration.type === "TSEnumDeclaration") exports.push({
41464
+ kind: "non-component",
41465
+ reportNode: declaration
41466
+ });
41467
+ }
40526
41468
  }
40527
- }
40528
- const isReExportFromSource = Boolean(child.source);
40529
- for (const specifier of child.specifiers ?? []) {
40530
- if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
40531
- const exported = specifier.exported;
40532
- const local = specifier.local;
40533
- let exportedName = null;
40534
- if (exported && isNodeOfType(exported, "Identifier")) exportedName = exported.name;
40535
- const localName = local && isNodeOfType(local, "Identifier") ? local.name : null;
40536
- const reportNode = specifier;
40537
- let entry;
40538
- if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
40539
- else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
40540
- else {
40541
- entry = {
40542
- kind: "non-component",
40543
- reportNode
40544
- };
40545
- if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
41469
+ const isReExportFromSource = Boolean(child.source);
41470
+ for (const specifier of child.specifiers ?? []) {
41471
+ if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
41472
+ const exported = specifier.exported;
41473
+ const local = specifier.local;
41474
+ let exportedName = null;
41475
+ if (exported && isNodeOfType(exported, "Identifier")) exportedName = exported.name;
41476
+ const localName = local && isNodeOfType(local, "Identifier") ? local.name : null;
41477
+ const reportNode = specifier;
41478
+ let entry;
41479
+ if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
41480
+ else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
41481
+ else {
41482
+ entry = {
41483
+ kind: "non-component",
41484
+ reportNode
41485
+ };
41486
+ if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
41487
+ }
41488
+ if (isReExportFromSource && entry.kind !== "react-component") continue;
41489
+ exports.push(entry);
40546
41490
  }
40547
- if (isReExportFromSource && entry.kind !== "react-component") continue;
40548
- exports.push(entry);
40549
41491
  }
40550
41492
  }
40551
- }
40552
- for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
40553
- for (const entry of exports) if (entry.kind === "namespace-object") context.report({
40554
- node: entry.reportNode,
40555
- message: NAMESPACE_OBJECT_MESSAGE
40556
- });
40557
- if (hasAnyExports && hasReactExport) for (const entry of exports) {
40558
- if (entry.kind === "non-component") context.report({
40559
- node: entry.reportNode,
40560
- message: NAMED_EXPORT_MESSAGE
40561
- });
40562
- if (entry.kind === "react-context") context.report({
41493
+ for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
41494
+ for (const entry of exports) if (entry.kind === "namespace-object") context.report({
40563
41495
  node: entry.reportNode,
40564
- message: REACT_CONTEXT_MESSAGE
41496
+ message: NAMESPACE_OBJECT_MESSAGE
40565
41497
  });
41498
+ if (hasAnyExports && hasReactExport) for (const entry of exports) {
41499
+ if (entry.kind === "non-component") context.report({
41500
+ node: entry.reportNode,
41501
+ message: NAMED_EXPORT_MESSAGE
41502
+ });
41503
+ if (entry.kind === "react-context") context.report({
41504
+ node: entry.reportNode,
41505
+ message: REACT_CONTEXT_MESSAGE
41506
+ });
41507
+ }
40566
41508
  }
40567
- } };
41509
+ };
40568
41510
  }
40569
41511
  });
40570
41512
  //#endregion
@@ -40666,7 +41608,12 @@ const postmessageOriginRisk = defineRule({
40666
41608
  scan: (file) => {
40667
41609
  if (!isProductionSourcePath(file.relativePath)) return [];
40668
41610
  if (WORKER_FILE_PATH_PATTERN.test(file.relativePath)) return [];
40669
- const ast = parseSourceText(file.absolutePath, file.content);
41611
+ if (!file.content.includes("addEventListener") && !file.content.includes("onmessage")) return [];
41612
+ const ast = parseSourceText({
41613
+ filename: file.absolutePath,
41614
+ sourceText: file.content,
41615
+ shouldAttachParentReferences: false
41616
+ });
40670
41617
  if (ast === null) return [];
40671
41618
  const findings = [];
40672
41619
  walkAst(ast, (node) => {
@@ -40916,7 +41863,7 @@ const preactPreferOndblclick = defineRule({
40916
41863
  recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
40917
41864
  create: (context) => ({ JSXOpeningElement(node) {
40918
41865
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
40919
- const tagName = node.name.name;
41866
+ const tagName = resolveJsxElementType(node);
40920
41867
  if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
40921
41868
  const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
40922
41869
  if (!onDoubleClickAttribute) return;
@@ -40936,7 +41883,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
40936
41883
  ]);
40937
41884
  const isTextLikeInput = (openingElement) => {
40938
41885
  if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
40939
- const tagName = openingElement.name.name;
41886
+ const tagName = resolveJsxElementType(openingElement);
40940
41887
  if (tagName === "textarea") return true;
40941
41888
  if (tagName !== "input") return false;
40942
41889
  const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
@@ -41093,8 +42040,9 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
41093
42040
  ]);
41094
42041
  const collectBooleanPropBindings = (param) => {
41095
42042
  const bindings = /* @__PURE__ */ new Set();
41096
- if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
41097
- 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 ?? []) {
41098
42046
  if (!isNodeOfType(property, "Property")) continue;
41099
42047
  if (property.computed) continue;
41100
42048
  if (!isNodeOfType(property.key, "Identifier")) continue;
@@ -41355,7 +42303,7 @@ const preferHtmlDialog = defineRule({
41355
42303
  },
41356
42304
  JSXOpeningElement(node) {
41357
42305
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
41358
- const tagName = node.name.name;
42306
+ const tagName = resolveJsxElementType(node);
41359
42307
  if (tagName === "dialog") return;
41360
42308
  if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
41361
42309
  if (!HTML_TAGS.has(tagName)) return;
@@ -42902,13 +43850,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
42902
43850
  }
42903
43851
  return null;
42904
43852
  };
42905
- const resolveHookNameFromInitializer = (initializer) => {
43853
+ const resolveHookNameFromInitializer = (initializer, scopes) => {
42906
43854
  if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
42907
43855
  if (!isNodeOfType(initializer, "Identifier")) return null;
42908
- const binding = findVariableInitializer(initializer, initializer.name);
42909
- if (!binding?.initializer || !isConstDeclaredBinding(binding)) return null;
42910
- if (!isNodeOfType(binding.initializer, "CallExpression")) return null;
42911
- 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);
42912
43860
  };
42913
43861
  const queryNoRestDestructuring = defineRule({
42914
43862
  id: "query-no-rest-destructuring",
@@ -42921,7 +43869,7 @@ const queryNoRestDestructuring = defineRule({
42921
43869
  if (!isNodeOfType(node.id, "ObjectPattern")) return;
42922
43870
  if (!node.init) return;
42923
43871
  if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
42924
- const hookName = resolveHookNameFromInitializer(node.init);
43872
+ const hookName = resolveHookNameFromInitializer(node.init, context.scopes);
42925
43873
  if (!hookName) return;
42926
43874
  context.report({
42927
43875
  node: node.id,
@@ -43046,15 +43994,6 @@ const isStableHookWrapperArgument = (node) => {
43046
43994
  const parent = node.parent;
43047
43995
  return isNodeOfType(parent, "CallExpression") && isNodeOfType(parent.callee, "Identifier") && STABLE_HOOK_WRAPPERS.has(parent.callee.name) && Boolean(parent.arguments?.some((argument) => argument === node));
43048
43996
  };
43049
- const isImmediatelyInvokedFunction = (functionNode) => {
43050
- let wrappedCallee = functionNode;
43051
- let enclosing = functionNode.parent;
43052
- while (enclosing && stripParenExpression(enclosing) === functionNode) {
43053
- wrappedCallee = enclosing;
43054
- enclosing = enclosing.parent ?? null;
43055
- }
43056
- return Boolean(enclosing && isNodeOfType(enclosing, "CallExpression") && enclosing.callee === wrappedCallee);
43057
- };
43058
43997
  const queryStableQueryClient = defineRule({
43059
43998
  id: "query-stable-query-client",
43060
43999
  title: "Unstable QueryClient in component",
@@ -43401,7 +44340,7 @@ const renderingAnimateSvgWrapper = defineRule({
43401
44340
  severity: "warn",
43402
44341
  recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
43403
44342
  create: (context) => ({ JSXOpeningElement(node) {
43404
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
44343
+ if (resolveJsxElementType(node) !== "svg") return;
43405
44344
  if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
43406
44345
  node,
43407
44346
  message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
@@ -44694,14 +45633,10 @@ const rerenderLazyStateInit = defineRule({
44694
45633
  //#endregion
44695
45634
  //#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
44696
45635
  const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
44697
- const callbackReturnsJsx = (callback) => {
45636
+ const callbackReturnsJsx = (callback, scopes) => {
44698
45637
  if (!callback) return false;
44699
45638
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
44700
- const body = callback.body;
44701
- if (isJsxExpression(body)) return true;
44702
- if (!isNodeOfType(body, "BlockStatement")) return false;
44703
- for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
44704
- return false;
45639
+ return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
44705
45640
  };
44706
45641
  const returnArgumentUsesAnyName = (returnStatement, names) => {
44707
45642
  if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
@@ -44779,7 +45714,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
44779
45714
  if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
44780
45715
  for (const declarator of stmt.declarations ?? []) {
44781
45716
  const init = declarator.init;
44782
- 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)) {
44783
45718
  memoNode = declarator;
44784
45719
  callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
44785
45720
  if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
@@ -44845,8 +45780,11 @@ const collectFromObjectPattern = (pattern, bindings) => {
44845
45780
  const collectDefaultedEmptyBindings = (functionNode) => {
44846
45781
  const bindings = /* @__PURE__ */ new Map();
44847
45782
  const params = functionNode.params ?? [];
44848
- for (const param of params) collectFromObjectPattern(param, bindings);
44849
- 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]);
44850
45788
  const body = functionNode.body;
44851
45789
  if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
44852
45790
  if (!isNodeOfType(body, "BlockStatement")) return bindings;
@@ -45352,8 +46290,8 @@ const rnAnimationReactionAsDerived = defineRule({
45352
46290
  if (statements.length !== 1) return;
45353
46291
  const onlyStatement = statements[0];
45354
46292
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
45355
- singleAssignment = onlyStatement.expression;
45356
- } else if (body) singleAssignment = body;
46293
+ singleAssignment = unwrapDiscardedExpression(onlyStatement);
46294
+ } else if (body) singleAssignment = unwrapDiscardedExpression(body);
45357
46295
  if (!singleAssignment) return;
45358
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";
45359
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;
@@ -52273,7 +53211,8 @@ const serverCacheWithObjectLiteral = defineRule({
52273
53211
  if (!isNodeOfType(node.callee, "Identifier")) return;
52274
53212
  if (!cachedFunctionNames.has(node.callee.name)) return;
52275
53213
  const firstArg = node.arguments?.[0];
52276
- 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;
52277
53216
  context.report({
52278
53217
  node,
52279
53218
  message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
@@ -52960,10 +53899,6 @@ const isInvalidStyleExpression = (expression) => {
52960
53899
  }
52961
53900
  return false;
52962
53901
  };
52963
- const getJsxOpeningElementName = (node) => {
52964
- if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
52965
- return null;
52966
- };
52967
53902
  const stylePropObject = defineRule({
52968
53903
  id: "style-prop-object",
52969
53904
  title: "Style prop is not an object",
@@ -52975,7 +53910,8 @@ const stylePropObject = defineRule({
52975
53910
  const allowSet = new Set(allow);
52976
53911
  return {
52977
53912
  JSXOpeningElement(node) {
52978
- const elementName = getJsxOpeningElementName(node);
53913
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
53914
+ const elementName = resolveJsxElementType(node);
52979
53915
  if (elementName && allowSet.has(elementName)) return;
52980
53916
  if (elementName) {
52981
53917
  const firstCharCode = elementName.charCodeAt(0);
@@ -53680,7 +54616,7 @@ const tanstackStartNoAnchorElement = defineRule({
53680
54616
  create: (context) => {
53681
54617
  if (!isInProjectDirectory(context, "routes")) return {};
53682
54618
  return { JSXOpeningElement(node) {
53683
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
54619
+ if (resolveJsxElementType(node) !== "a") return;
53684
54620
  const attributes = node.attributes ?? [];
53685
54621
  const hrefAttribute = findJsxAttribute(attributes, "href");
53686
54622
  if (!hrefAttribute?.value) return;
@@ -54301,8 +55237,7 @@ const voidDomElementsNoChildren = defineRule({
54301
55237
  create: (context) => ({
54302
55238
  JSXElement(node) {
54303
55239
  const openingElement = node.openingElement;
54304
- if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
54305
- const tagName = openingElement.name.name;
55240
+ const tagName = resolveJsxElementType(openingElement);
54306
55241
  if (!VOID_DOM_ELEMENTS.has(tagName)) return;
54307
55242
  const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
54308
55243
  const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
@@ -54367,12 +55302,6 @@ const webhookSignatureRisk = defineRule({
54367
55302
  //#region src/plugin/rules/zod/utils/zod-ast.ts
54368
55303
  const ZOD_MODULE = "zod";
54369
55304
  const ZOD_MODULE_SOURCES = [ZOD_MODULE];
54370
- const getStaticPropertyName = (member) => {
54371
- const property = member.property;
54372
- if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
54373
- if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
54374
- return null;
54375
- };
54376
55305
  const importInfoCache = /* @__PURE__ */ new WeakMap();
54377
55306
  const getImportInfoForIdentifier = (identifier) => {
54378
55307
  if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
@@ -59553,7 +60482,10 @@ const appendNode = (builder, block, node) => {
59553
60482
  };
59554
60483
  const mapDescendantsToBlock = (builder, node, block) => {
59555
60484
  builder.nodeBlock.set(node, block);
59556
- if (isFunctionLike$1(node)) return;
60485
+ if (isFunctionLike$1(node)) {
60486
+ builder.nestedFunctions.push(node);
60487
+ return;
60488
+ }
59557
60489
  const record = node;
59558
60490
  for (const key of Object.keys(record)) {
59559
60491
  if (key === "parent") continue;
@@ -59787,11 +60719,12 @@ const buildStatement = (builder, statement, current) => {
59787
60719
  mapDescendantsToBlock(builder, statement, current);
59788
60720
  return current;
59789
60721
  };
59790
- const buildFunctionCfg = (functionNode, body) => {
60722
+ const buildFunctionCfg = (functionNode, body, nestedFunctionSink) => {
59791
60723
  const builder = {
59792
60724
  blocks: [],
59793
60725
  entry: null,
59794
60726
  exit: null,
60727
+ nestedFunctions: nestedFunctionSink,
59795
60728
  nodeBlock: /* @__PURE__ */ new Map(),
59796
60729
  loopStack: [],
59797
60730
  switchStack: [],
@@ -59809,13 +60742,12 @@ const buildFunctionCfg = (functionNode, body) => {
59809
60742
  bodyEnd = entry;
59810
60743
  }
59811
60744
  addEdge(bodyEnd, exit, "uncond");
59812
- const blockOf = (node) => builder.nodeBlock.get(node) ?? null;
59813
60745
  return {
59814
60746
  owner: functionNode,
59815
60747
  entry,
59816
60748
  exit,
59817
60749
  blocks: builder.blocks,
59818
- blockOf
60750
+ blockOf: (node) => builder.nodeBlock.get(node) ?? null
59819
60751
  };
59820
60752
  };
59821
60753
  const computeUnconditionalSet = (cfg) => {
@@ -59852,8 +60784,9 @@ const computeUnconditionalSet = (cfg) => {
59852
60784
  const analyzeControlFlow = (program) => {
59853
60785
  nextBlockId = 0;
59854
60786
  const functionCfgs = /* @__PURE__ */ new Map();
60787
+ const pendingFunctions = [];
59855
60788
  const buildFor = (functionNode, body) => {
59856
- const cfg = buildFunctionCfg(functionNode, body);
60789
+ const cfg = buildFunctionCfg(functionNode, body, pendingFunctions);
59857
60790
  functionCfgs.set(functionNode, {
59858
60791
  cfg,
59859
60792
  unconditionalSet: computeUnconditionalSet(cfg)
@@ -59863,21 +60796,18 @@ const analyzeControlFlow = (program) => {
59863
60796
  type: "BlockStatement",
59864
60797
  body: program.body
59865
60798
  });
59866
- const visit = (node) => {
59867
- if (isFunctionLike$1(node)) {
59868
- const body = node.body;
59869
- if (body) buildFor(node, body);
59870
- }
59871
- const record = node;
59872
- for (const key of Object.keys(record)) {
59873
- if (key === "parent") continue;
59874
- const child = record[key];
59875
- if (Array.isArray(child)) {
59876
- for (const item of child) if (isAstNode(item)) visit(item);
59877
- } else if (isAstNode(child)) visit(child);
59878
- }
60799
+ for (let functionIndex = 0; functionIndex < pendingFunctions.length; functionIndex += 1) {
60800
+ const functionNode = pendingFunctions[functionIndex];
60801
+ if (!isFunctionLike$1(functionNode) || !functionNode.body || functionCfgs.has(functionNode)) continue;
60802
+ buildFor(functionNode, functionNode.body);
60803
+ }
60804
+ const getFunctionEntry = (functionNode) => {
60805
+ const existingEntry = functionCfgs.get(functionNode);
60806
+ if (existingEntry) return existingEntry;
60807
+ if (!isFunctionLike$1(functionNode) || !functionNode.body) return null;
60808
+ buildFor(functionNode, functionNode.body);
60809
+ return functionCfgs.get(functionNode) ?? null;
59879
60810
  };
59880
- visit(program);
59881
60811
  const enclosingFunction = (node) => {
59882
60812
  let current = node;
59883
60813
  while (current) {
@@ -59888,12 +60818,12 @@ const analyzeControlFlow = (program) => {
59888
60818
  return null;
59889
60819
  };
59890
60820
  const cfgFor = (functionLike) => {
59891
- return functionCfgs.get(functionLike)?.cfg ?? null;
60821
+ return getFunctionEntry(functionLike)?.cfg ?? null;
59892
60822
  };
59893
60823
  const isUnconditionalFromEntry = (node) => {
59894
60824
  const owner = enclosingFunction(node);
59895
60825
  if (!owner) return true;
59896
- const entry = functionCfgs.get(owner);
60826
+ const entry = getFunctionEntry(owner);
59897
60827
  if (!entry) return true;
59898
60828
  const block = entry.cfg.blockOf(node);
59899
60829
  if (!block) return true;
@@ -59967,17 +60897,14 @@ const wrapWithSemanticContext = (rule) => ({
59967
60897
  }
59968
60898
  };
59969
60899
  const visitors = rule.create(enrichedContext);
59970
- const passthroughVisitors = {};
59971
- for (const [nodeType, handler] of Object.entries(visitors)) {
59972
- if (typeof handler !== "function") continue;
59973
- passthroughVisitors[nodeType] = handler;
59974
- }
59975
- const innerProgramHandler = passthroughVisitors.Program;
59976
- passthroughVisitors.Program = ((node) => {
59977
- programRoot = node;
59978
- if (innerProgramHandler) innerProgramHandler(node);
59979
- });
59980
- return passthroughVisitors;
60900
+ const innerProgramHandler = visitors.Program;
60901
+ return {
60902
+ ...visitors,
60903
+ Program: ((node) => {
60904
+ programRoot = node;
60905
+ if (innerProgramHandler) innerProgramHandler(node);
60906
+ })
60907
+ };
59981
60908
  }
59982
60909
  });
59983
60910
  //#endregion