oxlint-plugin-react-doctor 0.7.9-dev.db5fe10 → 0.7.9-dev.e5a5f73

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 (3) hide show
  1. package/dist/index.d.ts +46 -0
  2. package/dist/index.js +2013 -308
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { KEYS } from "eslint-visitor-keys";
2
2
  import * as path from "node:path";
3
+ import { parseSync, visitorKeys } from "oxc-parser";
3
4
  import * as fs from "node:fs";
4
5
  import { readFileSync } from "node:fs";
5
- import { parseSync, visitorKeys } from "oxc-parser";
6
6
  import { analyze } from "eslint-scope";
7
7
  //#region src/plugin/utils/is-node-of-type.ts
8
8
  const isNodeOfType = (node, type) => node !== null && typeof node === "object" && "type" in node && node.type === type;
@@ -327,7 +327,15 @@ const defineRule = (rule) => {
327
327
  let lastContentLineIndex;
328
328
  const buildLineStartOffsets = (content) => {
329
329
  const lineStartOffsets = [0];
330
- for (let newlineIndex = content.indexOf("\n"); newlineIndex !== -1; newlineIndex = content.indexOf("\n", newlineIndex + 1)) lineStartOffsets.push(newlineIndex + 1);
330
+ for (let characterIndex = 0; characterIndex < content.length; characterIndex += 1) {
331
+ const character = content[characterIndex];
332
+ if (character === "\r" && content[characterIndex + 1] === "\n") {
333
+ characterIndex += 1;
334
+ lineStartOffsets.push(characterIndex + 1);
335
+ continue;
336
+ }
337
+ if (character === "\r" || character === "\n" || character === "\u2028" || character === "\u2029") lineStartOffsets.push(characterIndex + 1);
338
+ }
331
339
  return lineStartOffsets;
332
340
  };
333
341
  const getLineStartOffsets = (content) => {
@@ -4342,7 +4350,8 @@ const SECRET_VALUE_PATTERNS = [
4342
4350
  ];
4343
4351
  const JWT_LITERAL_VALUE_PATTERN = /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{16,}\b/;
4344
4352
  const PUBLIC_ENV_SECRET_NAME_PATTERN = /\b(?:NEXT_PUBLIC|VITE|REACT_APP|EXPO_PUBLIC)_[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PRIVATE|DATABASE_URL|SERVICE_ROLE|AWS_ACCESS_KEY|AWS_SECRET)[A-Z0-9_]*\b/i;
4345
- const FULL_ENV_LEAK_CONTEXT_PATTERN = /\b(?:process\.env|import\.meta\.env|window\.__[A-Z0-9_]*ENV[A-Z0-9_]*__|__[A-Z0-9_]*ENV[A-Z0-9_]*__)\b/;
4353
+ const FULL_ENV_LEAK_CONTEXT_PATTERN = /\b(?:process\s*\.\s*env|import\s*\.\s*meta\s*\.\s*env|window\.__[A-Z0-9_]*ENV[A-Z0-9_]*__|__[A-Z0-9_]*ENV[A-Z0-9_]*__)\b/;
4354
+ const FULL_ENV_LEAK_COMMENT_TRIVIA_PATTERN = /\b(?:(?:process|window)\s*(?:\/[*/]|<!--|-->|\.\s*(?:\/[*/]|<!--|-->))|import\s*(?:\/[*/]|<!--|-->|\.\s*(?:\/[*/]|<!--|-->|meta\s*(?:\/[*/]|<!--|-->|\.\s*(?:\/[*/]|<!--|-->)))))/;
4346
4355
  const FULL_ENV_LEAK_SECRET_NAME_PATTERN = /\b(?:DATABASE_URL|AWS_SECRET_ACCESS_KEY|AWS_ACCESS_KEY_ID|MAILGUN_API_KEY|SALESFORCE_CLIENT_SECRET|OKTA_CLIENT_SECRET|SESSION_SECRET|COOKIE_SECRET|PRIVATE_KEY|SERVICE_ROLE)\b/;
4347
4356
  const TRUSTED_PUBLIC_SECRET_NAME_PATTERN = /(?:SENTRY_DSN|PUBLIC_KEY|PUBLISHABLE|ANON_KEY|POSTHOG_(?:PROJECT_)?TOKEN|POSTHOG_KEY|TLDRAW_LICENSE_KEY|CLERK_PUBLISHABLE_KEY|ALGOLIA_SEARCH_KEY|GC_API_KEY|GOOGLE_MAPS_API_KEY|MAPBOX_TOKEN|MIXPANEL_TOKEN|FACEBOOK_CLIENT_TOKEN|(?:NEXT_PUBLIC|VITE|REACT_APP|EXPO_PUBLIC)_(?:DISABLE|ENABLE|ALLOW|REQUIRE)_)|(?:TOKEN|SECRET|PASSWORD|PRIVATE)_(?:KIND|TYPE|URL|URI|ENDPOINT|HEADER|NAME)$/i;
4348
4357
  const PUBLIC_CLIENT_KEY_PATTERNS = [
@@ -4500,6 +4509,172 @@ const findSuspiciousPublicEnvSecretNamePattern = (content) => {
4500
4509
  //#region src/plugin/rules/security-scan/utils/has-full-env-leak-shape.ts
4501
4510
  const hasFullEnvLeakShape = (content) => FULL_ENV_LEAK_CONTEXT_PATTERN.test(content) && FULL_ENV_LEAK_SECRET_NAME_PATTERN.test(content);
4502
4511
  //#endregion
4512
+ //#region src/plugin/utils/attach-parent-references.ts
4513
+ const attachParentReferences = (root) => {
4514
+ const visit = (node, parent) => {
4515
+ const writableNode = node;
4516
+ writableNode.parent = parent;
4517
+ const nodeRecord = node;
4518
+ for (const key of Object.keys(nodeRecord)) {
4519
+ if (key === "parent") continue;
4520
+ const child = nodeRecord[key];
4521
+ if (Array.isArray(child)) {
4522
+ for (const item of child) if (isAstNode(item)) visit(item, node);
4523
+ } else if (isAstNode(child)) visit(child, node);
4524
+ }
4525
+ };
4526
+ visit(root, null);
4527
+ };
4528
+ //#endregion
4529
+ //#region src/plugin/utils/cross-file-probe-recorder.ts
4530
+ let activeProbeTrace = null;
4531
+ const recordExistenceProbe = (absolutePath) => {
4532
+ activeProbeTrace?.existencePaths.add(absolutePath);
4533
+ };
4534
+ const recordContentProbe = (absolutePath) => {
4535
+ activeProbeTrace?.contentPaths.add(absolutePath);
4536
+ };
4537
+ const isProbeRecorderActive = () => activeProbeTrace !== null;
4538
+ const collectCrossFileProbes = (collect) => {
4539
+ const previousTrace = activeProbeTrace;
4540
+ const trace = {
4541
+ existencePaths: /* @__PURE__ */ new Set(),
4542
+ contentPaths: /* @__PURE__ */ new Set()
4543
+ };
4544
+ activeProbeTrace = trace;
4545
+ try {
4546
+ collect();
4547
+ } finally {
4548
+ activeProbeTrace = previousTrace;
4549
+ }
4550
+ return trace;
4551
+ };
4552
+ //#endregion
4553
+ //#region src/plugin/utils/parse-source-file.ts
4554
+ const FILENAME_TO_LANG = {
4555
+ ".ts": "ts",
4556
+ ".tsx": "tsx",
4557
+ ".js": "js",
4558
+ ".jsx": "jsx",
4559
+ ".mjs": "js",
4560
+ ".cjs": "js",
4561
+ ".mts": "ts",
4562
+ ".cts": "ts"
4563
+ };
4564
+ const resolveLang = (filename) => {
4565
+ return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
4566
+ };
4567
+ const parseSourceText = ({ filename, sourceText, shouldAttachParentReferences = true }) => {
4568
+ try {
4569
+ const result = parseSync(filename, sourceText, {
4570
+ astType: "ts",
4571
+ lang: resolveLang(filename)
4572
+ });
4573
+ if (result.errors.some((parseError) => parseError.severity === "Error")) return null;
4574
+ const parsedProgram = result.program;
4575
+ if (shouldAttachParentReferences) attachParentReferences(parsedProgram);
4576
+ return parsedProgram;
4577
+ } catch {
4578
+ return null;
4579
+ }
4580
+ };
4581
+ const parseCache = /* @__PURE__ */ new Map();
4582
+ const parseSourceFile = (absoluteFilePath) => {
4583
+ if (!(absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts"))) recordContentProbe(absoluteFilePath);
4584
+ let fileStat;
4585
+ try {
4586
+ fileStat = fs.statSync(absoluteFilePath);
4587
+ } catch {
4588
+ return null;
4589
+ }
4590
+ if (!fileStat.isFile()) return null;
4591
+ if (fileStat.size > 2e6) return null;
4592
+ const cached = parseCache.get(absoluteFilePath);
4593
+ if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
4594
+ if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
4595
+ parseCache.set(absoluteFilePath, {
4596
+ mtimeMs: fileStat.mtimeMs,
4597
+ size: fileStat.size,
4598
+ program: null
4599
+ });
4600
+ return null;
4601
+ }
4602
+ let sourceText;
4603
+ try {
4604
+ sourceText = fs.readFileSync(absoluteFilePath, "utf8");
4605
+ } catch {
4606
+ parseCache.set(absoluteFilePath, {
4607
+ mtimeMs: fileStat.mtimeMs,
4608
+ size: fileStat.size,
4609
+ program: null
4610
+ });
4611
+ return null;
4612
+ }
4613
+ const parsedProgram = parseSourceText({
4614
+ filename: absoluteFilePath,
4615
+ sourceText
4616
+ });
4617
+ parseCache.set(absoluteFilePath, {
4618
+ mtimeMs: fileStat.mtimeMs,
4619
+ size: fileStat.size,
4620
+ program: parsedProgram
4621
+ });
4622
+ return parsedProgram;
4623
+ };
4624
+ //#endregion
4625
+ //#region src/plugin/rules/security-scan/utils/mask-source-comments.ts
4626
+ const SOURCE_FILE_EXTENSION_PATTERN$1 = /\.(?:[cm]?[jt]sx?)$/i;
4627
+ const POSSIBLE_SOURCE_COMMENT_PATTERN = /\/\/|\/\*|<!--/;
4628
+ const LINE_TERMINATORS = new Set([
4629
+ "\r",
4630
+ "\n",
4631
+ "\u2028",
4632
+ "\u2029"
4633
+ ]);
4634
+ const hasPossibleAnnexBClosingComment = (content) => {
4635
+ let searchIndex = 0;
4636
+ while (searchIndex < content.length) {
4637
+ const closingCommentIndex = content.indexOf("-->", searchIndex);
4638
+ if (closingCommentIndex === -1) return false;
4639
+ let prefixIndex = closingCommentIndex - 1;
4640
+ while (prefixIndex >= 0 && !LINE_TERMINATORS.has(content[prefixIndex] ?? "")) {
4641
+ if (content[prefixIndex]?.trim() !== "") break;
4642
+ prefixIndex -= 1;
4643
+ }
4644
+ if (prefixIndex < 0 || LINE_TERMINATORS.has(content[prefixIndex] ?? "")) return true;
4645
+ searchIndex = closingCommentIndex + 3;
4646
+ }
4647
+ return false;
4648
+ };
4649
+ const maskSourceComments = (relativePath, content) => {
4650
+ if (!SOURCE_FILE_EXTENSION_PATTERN$1.test(relativePath)) return content;
4651
+ if (!content.startsWith("#!") && !POSSIBLE_SOURCE_COMMENT_PATTERN.test(content) && !hasPossibleAnnexBClosingComment(content)) return content;
4652
+ try {
4653
+ const result = parseSync(relativePath, content, {
4654
+ astType: "ts",
4655
+ lang: resolveLang(relativePath)
4656
+ });
4657
+ if (result.errors.some((parseError) => parseError.severity === "Error")) return void 0;
4658
+ const firstLineTerminatorIndex = content.search(/[\r\n\u2028\u2029]/);
4659
+ const ignoredRanges = [...content.startsWith("#!") ? [{
4660
+ start: 0,
4661
+ end: firstLineTerminatorIndex === -1 ? content.length : firstLineTerminatorIndex
4662
+ }] : [], ...result.comments];
4663
+ if (ignoredRanges.length === 0) return content;
4664
+ const contentParts = [];
4665
+ let previousEnd = 0;
4666
+ for (const ignoredRange of ignoredRanges) {
4667
+ contentParts.push(content.slice(previousEnd, ignoredRange.start));
4668
+ contentParts.push(content.slice(ignoredRange.start, ignoredRange.end).replace(/[^\r\n\u2028\u2029]/g, " "));
4669
+ previousEnd = ignoredRange.end;
4670
+ }
4671
+ contentParts.push(content.slice(previousEnd));
4672
+ return contentParts.join("");
4673
+ } catch {
4674
+ return;
4675
+ }
4676
+ };
4677
+ //#endregion
4503
4678
  //#region src/plugin/rules/security-scan/utils/scan-artifact-leak.ts
4504
4679
  const scanArtifactLeak = (file, findLeakPattern, message) => {
4505
4680
  if (DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath)) return [];
@@ -4515,12 +4690,39 @@ const scanArtifactLeak = (file, findLeakPattern, message) => {
4515
4690
  };
4516
4691
  //#endregion
4517
4692
  //#region src/plugin/rules/security-scan/artifact-env-leak.ts
4693
+ const ARTIFACT_ENV_LEAK_MESSAGE = "A browser artifact contains server-secret environment names or a full environment dump shape.";
4694
+ const findArtifactEnvLeakPattern = (content) => findSuspiciousPublicEnvSecretNamePattern(content) ?? (hasFullEnvLeakShape(content) ? FULL_ENV_LEAK_SECRET_NAME_PATTERN : void 0);
4518
4695
  const artifactEnvLeak = defineRule({
4519
4696
  id: "artifact-env-leak",
4520
4697
  title: "Server env leaked to browser artifact",
4521
4698
  severity: "error",
4522
4699
  recommendation: "Treat public env prefixes as publication, not secrecy; keep secret env vars server-only and rebuild after rotating leaked keys.",
4523
- scan: (file) => scanArtifactLeak(file, (content) => findSuspiciousPublicEnvSecretNamePattern(content) ?? (hasFullEnvLeakShape(content) ? FULL_ENV_LEAK_SECRET_NAME_PATTERN : void 0), "A browser artifact contains server-secret environment names or a full environment dump shape.")
4700
+ scan: (file) => {
4701
+ let isRawCandidateExact = false;
4702
+ const findRawCandidatePattern = (content) => {
4703
+ const suspiciousPublicNamePattern = findSuspiciousPublicEnvSecretNamePattern(content);
4704
+ if (suspiciousPublicNamePattern) {
4705
+ isRawCandidateExact = true;
4706
+ return suspiciousPublicNamePattern;
4707
+ }
4708
+ if (!FULL_ENV_LEAK_SECRET_NAME_PATTERN.test(content)) return void 0;
4709
+ if (FULL_ENV_LEAK_CONTEXT_PATTERN.test(content)) {
4710
+ isRawCandidateExact = true;
4711
+ return FULL_ENV_LEAK_SECRET_NAME_PATTERN;
4712
+ }
4713
+ return FULL_ENV_LEAK_COMMENT_TRIVIA_PATTERN.test(content) ? FULL_ENV_LEAK_SECRET_NAME_PATTERN : void 0;
4714
+ };
4715
+ const rawCandidateFindings = scanArtifactLeak(file, findRawCandidatePattern, ARTIFACT_ENV_LEAK_MESSAGE);
4716
+ if (rawCandidateFindings.length === 0) return rawCandidateFindings;
4717
+ const rawFindings = isRawCandidateExact ? rawCandidateFindings : scanArtifactLeak(file, findArtifactEnvLeakPattern, ARTIFACT_ENV_LEAK_MESSAGE);
4718
+ const executableContent = maskSourceComments(file.relativePath, file.content);
4719
+ if (executableContent === void 0) return rawCandidateFindings;
4720
+ if (executableContent === file.content) return rawFindings;
4721
+ return scanArtifactLeak({
4722
+ ...file,
4723
+ content: executableContent
4724
+ }, findArtifactEnvLeakPattern, ARTIFACT_ENV_LEAK_MESSAGE);
4725
+ }
4524
4726
  });
4525
4727
  //#endregion
4526
4728
  //#region src/plugin/rules/security-scan/artifact-secret-leak.ts
@@ -6534,6 +6736,28 @@ const asyncParallel = defineRule({
6534
6736
  }
6535
6737
  });
6536
6738
  //#endregion
6739
+ //#region src/plugin/utils/collect-function-return-statements.ts
6740
+ const collectFunctionReturnStatements = (functionNode) => {
6741
+ if (!isFunctionLike$1(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return [];
6742
+ const returnStatements = [];
6743
+ walkAst(functionNode.body, (node) => {
6744
+ if (node !== functionNode.body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
6745
+ if (isNodeOfType(node, "ReturnStatement")) returnStatements.push(node);
6746
+ });
6747
+ return returnStatements;
6748
+ };
6749
+ //#endregion
6750
+ //#region src/plugin/utils/is-nullish-expression.ts
6751
+ const isNullishExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined" || isNodeOfType(expression, "UnaryExpression") && expression.operator === "void";
6752
+ //#endregion
6753
+ //#region src/plugin/utils/strip-this-parameter.ts
6754
+ const stripThisParameter = (parameters) => {
6755
+ const firstParameter = parameters[0];
6756
+ if (!firstParameter) return parameters;
6757
+ if (isNodeOfType(firstParameter, "Identifier") && firstParameter.name === "this") return parameters.slice(1);
6758
+ return parameters;
6759
+ };
6760
+ //#endregion
6537
6761
  //#region src/plugin/rules/security/auth-token-in-web-storage.ts
6538
6762
  const MESSAGE$60 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
6539
6763
  const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
@@ -6545,12 +6769,8 @@ const STORAGE_GLOBALS = new Set([
6545
6769
  const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
6546
6770
  const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
6547
6771
  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;
6548
- const PRODUCT_API_KEY_RECORDS_PATTERN = /(?:^|[._:-])(?:created|saved|integration|mailing)[-_]?api[-_]?keys$/i;
6549
- const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
6550
6772
  const isAuthCredentialKey = (key) => {
6551
- if (PRODUCT_API_KEY_RECORDS_PATTERN.test(key)) return false;
6552
6773
  if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
6553
- if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
6554
6774
  if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
6555
6775
  return true;
6556
6776
  };
@@ -6559,11 +6779,63 @@ const isDirectWebStorageObject = (node) => {
6559
6779
  if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && STORAGE_GLOBALS.has(node.object.name) && isNodeOfType(node.property, "Identifier")) return STORAGE_NAMES.has(node.property.name);
6560
6780
  return false;
6561
6781
  };
6562
- const isWebStorageObject = (node) => {
6563
- if (isDirectWebStorageObject(node)) return true;
6564
- if (!isNodeOfType(node, "Identifier")) return false;
6565
- const binding = findVariableInitializer(node, node.name);
6566
- return binding?.initializer ? isDirectWebStorageObject(binding.initializer) : false;
6782
+ const immutableInitializer = (identifier, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
6783
+ if (visitedIdentifiers.has(identifier)) return null;
6784
+ visitedIdentifiers.add(identifier);
6785
+ const binding = findVariableInitializer(identifier, identifier.name);
6786
+ if (!binding?.initializer) return null;
6787
+ if (isNodeOfType(binding.initializer, "FunctionDeclaration")) return binding.initializer;
6788
+ const declarator = binding.bindingIdentifier.parent;
6789
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
6790
+ const declaration = declarator.parent;
6791
+ if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
6792
+ if (declaration.kind !== "const") return null;
6793
+ const initializer = stripParenExpression(binding.initializer);
6794
+ if (!isNodeOfType(initializer, "Identifier")) return initializer;
6795
+ return immutableInitializer(initializer, visitedIdentifiers) ?? initializer;
6796
+ };
6797
+ const isWebStorageFactoryResult = (node, visitedNodes) => {
6798
+ const expression = stripParenExpression(node);
6799
+ if (isWebStorageObject(expression, new Set(visitedNodes))) return true;
6800
+ if (isNodeOfType(expression, "ConditionalExpression")) {
6801
+ const consequent = stripParenExpression(expression.consequent);
6802
+ const alternate = stripParenExpression(expression.alternate);
6803
+ return (isNullishExpression(consequent) || isWebStorageFactoryResult(consequent, visitedNodes)) && (isNullishExpression(alternate) || isWebStorageFactoryResult(alternate, visitedNodes)) && (!isNullishExpression(consequent) || !isNullishExpression(alternate));
6804
+ }
6805
+ if (isNodeOfType(expression, "LogicalExpression")) {
6806
+ if (expression.operator === "&&") return isWebStorageFactoryResult(expression.right, visitedNodes);
6807
+ const left = stripParenExpression(expression.left);
6808
+ const right = stripParenExpression(expression.right);
6809
+ const isLeftStorage = isWebStorageFactoryResult(left, visitedNodes);
6810
+ const isRightStorage = isWebStorageFactoryResult(right, visitedNodes);
6811
+ return (isLeftStorage || isNullishExpression(left)) && (isRightStorage || isNullishExpression(right)) && (isLeftStorage || isRightStorage);
6812
+ }
6813
+ return false;
6814
+ };
6815
+ const isWebStorageObject = (node, visitedNodes = /* @__PURE__ */ new Set()) => {
6816
+ const expression = stripParenExpression(node);
6817
+ if (visitedNodes.has(expression)) return false;
6818
+ visitedNodes.add(expression);
6819
+ if (isDirectWebStorageObject(expression)) return true;
6820
+ if (isNodeOfType(expression, "Identifier")) {
6821
+ const initializer = immutableInitializer(expression);
6822
+ return initializer ? isWebStorageObject(initializer, new Set(visitedNodes)) : false;
6823
+ }
6824
+ if (!isNodeOfType(expression, "CallExpression")) return false;
6825
+ const callee = stripParenExpression(expression.callee);
6826
+ if (!isNodeOfType(callee, "Identifier")) return false;
6827
+ const factory = immutableInitializer(callee);
6828
+ if (!isFunctionLike$1(factory)) return false;
6829
+ if (isNodeOfType(factory, "ArrowFunctionExpression") && !isNodeOfType(factory.body, "BlockStatement")) return isWebStorageFactoryResult(factory.body, visitedNodes);
6830
+ let didReturnWebStorage = false;
6831
+ for (const returnStatement of collectFunctionReturnStatements(factory)) {
6832
+ if (!returnStatement.argument) continue;
6833
+ const strippedReturn = stripParenExpression(returnStatement.argument);
6834
+ if (isNullishExpression(strippedReturn)) continue;
6835
+ if (!isWebStorageFactoryResult(strippedReturn, visitedNodes)) return false;
6836
+ didReturnWebStorage = true;
6837
+ }
6838
+ return didReturnWebStorage;
6567
6839
  };
6568
6840
  const resolveStaticKeyString = (node) => {
6569
6841
  if (isNodeOfType(node, "Literal") && typeof node.value === "string") return node.value;
@@ -6583,6 +6855,56 @@ const staticMemberName = (member) => {
6583
6855
  if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
6584
6856
  return null;
6585
6857
  };
6858
+ const parameterIndex = (expression, parameterSymbolIds, scopes, canUnwrapSerialization, visitedNodes = /* @__PURE__ */ new Set()) => {
6859
+ const strippedExpression = stripParenExpression(expression);
6860
+ if (visitedNodes.has(strippedExpression)) return null;
6861
+ visitedNodes.add(strippedExpression);
6862
+ if (isNodeOfType(strippedExpression, "Identifier")) {
6863
+ const directSymbolId = scopes.symbolFor(strippedExpression)?.id;
6864
+ const directIndex = directSymbolId === void 0 ? -1 : parameterSymbolIds.indexOf(directSymbolId);
6865
+ if (directIndex !== -1) return directIndex;
6866
+ const initializer = immutableInitializer(strippedExpression);
6867
+ return initializer ? parameterIndex(initializer, parameterSymbolIds, scopes, canUnwrapSerialization, visitedNodes) : null;
6868
+ }
6869
+ if (canUnwrapSerialization && isNodeOfType(strippedExpression, "CallExpression") && isNodeOfType(strippedExpression.callee, "MemberExpression") && !strippedExpression.callee.computed && isNodeOfType(strippedExpression.callee.object, "Identifier") && strippedExpression.callee.object.name === "JSON" && isNodeOfType(strippedExpression.callee.property, "Identifier") && strippedExpression.callee.property.name === "stringify") {
6870
+ const serializedArgument = strippedExpression.arguments[0];
6871
+ return serializedArgument ? parameterIndex(serializedArgument, parameterSymbolIds, scopes, true, visitedNodes) : null;
6872
+ }
6873
+ return null;
6874
+ };
6875
+ const storageHelperSinkCache = /* @__PURE__ */ new WeakMap();
6876
+ const findStorageHelperSinks = (functionNode, scopes) => {
6877
+ const cachedSinks = storageHelperSinkCache.get(functionNode);
6878
+ if (cachedSinks) return cachedSinks;
6879
+ if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) {
6880
+ storageHelperSinkCache.set(functionNode, []);
6881
+ return [];
6882
+ }
6883
+ const parameterSymbolIds = stripThisParameter(functionNode.params).map((parameter) => {
6884
+ const strippedParameter = stripParenExpression(parameter);
6885
+ const identifier = isNodeOfType(strippedParameter, "Identifier") ? strippedParameter : isNodeOfType(strippedParameter, "AssignmentPattern") && isNodeOfType(strippedParameter.left, "Identifier") ? strippedParameter.left : null;
6886
+ return identifier ? scopes.symbolFor(identifier)?.id ?? null : null;
6887
+ });
6888
+ const helperSinks = [];
6889
+ walkAst(functionNode.body, (child) => {
6890
+ if (child !== functionNode.body && isFunctionLike$1(child)) return false;
6891
+ if (!isNodeOfType(child, "CallExpression")) return;
6892
+ const callee = stripParenExpression(child.callee);
6893
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem" || !isWebStorageObject(callee.object)) return;
6894
+ const keyExpression = child.arguments[0];
6895
+ const valueExpression = child.arguments[1];
6896
+ if (!keyExpression || !valueExpression) return;
6897
+ const keyParameterIndex = parameterIndex(keyExpression, parameterSymbolIds, scopes, false);
6898
+ const valueParameterIndex = parameterIndex(valueExpression, parameterSymbolIds, scopes, true);
6899
+ if (keyParameterIndex === null || valueParameterIndex === null) return;
6900
+ helperSinks.push({
6901
+ keyParameterIndex,
6902
+ valueParameterIndex
6903
+ });
6904
+ });
6905
+ storageHelperSinkCache.set(functionNode, helperSinks);
6906
+ return helperSinks;
6907
+ };
6586
6908
  const authTokenInWebStorage = defineRule({
6587
6909
  id: "auth-token-in-web-storage",
6588
6910
  title: "Auth token in web storage",
@@ -6590,14 +6912,23 @@ const authTokenInWebStorage = defineRule({
6590
6912
  recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
6591
6913
  create: skipNonProductionFiles((context) => ({
6592
6914
  CallExpression(node) {
6593
- const callee = node.callee;
6594
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
6595
- if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
6596
- if (!isWebStorageObject(stripParenExpression(callee.object))) return;
6597
- const keyArgument = node.arguments?.[0];
6598
- if (!keyArgument) return;
6599
- const keyString = resolveStaticKeyString(keyArgument);
6600
- if (keyString === null || !isAuthCredentialKey(keyString)) return;
6915
+ const callee = stripParenExpression(node.callee);
6916
+ const keyArguments = [];
6917
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "setItem" && isWebStorageObject(stripParenExpression(callee.object))) {
6918
+ const keyArgument = node.arguments[0];
6919
+ if (keyArgument) keyArguments.push(keyArgument);
6920
+ } else if (isNodeOfType(callee, "Identifier")) {
6921
+ const helperFunction = immutableInitializer(callee);
6922
+ const helperSinks = helperFunction ? findStorageHelperSinks(helperFunction, context.scopes) : [];
6923
+ for (const helperSink of helperSinks) {
6924
+ const keyArgument = node.arguments[helperSink.keyParameterIndex];
6925
+ if (keyArgument && node.arguments[helperSink.valueParameterIndex]) keyArguments.push(keyArgument);
6926
+ }
6927
+ }
6928
+ if (!keyArguments.some((keyArgument) => {
6929
+ const keyString = resolveStaticKeyString(keyArgument);
6930
+ return keyString !== null && isAuthCredentialKey(keyString);
6931
+ })) return;
6601
6932
  context.report({
6602
6933
  node,
6603
6934
  message: MESSAGE$60
@@ -6845,9 +7176,6 @@ const isCreateElementCall = (node) => {
6845
7176
  return false;
6846
7177
  };
6847
7178
  //#endregion
6848
- //#region src/plugin/utils/is-nullish-expression.ts
6849
- const isNullishExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined" || isNodeOfType(expression, "UnaryExpression") && expression.operator === "void";
6850
- //#endregion
6851
7179
  //#region src/plugin/rules/react-builtins/button-has-type.ts
6852
7180
  const MISSING_MESSAGE$2 = "Your users can submit the form by accident because a `<button>` with no `type` defaults to submit.";
6853
7181
  const INVALID_MESSAGE = "This button has an invalid `type`, so the browser may treat it like a submit button.";
@@ -7309,6 +7637,7 @@ const areExpressionsStructurallyEqual = (a, b) => {
7309
7637
  if (a.type !== b.type) return false;
7310
7638
  if (isNodeOfType(a, "ThisExpression")) return true;
7311
7639
  if (isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier")) return a.name === b.name;
7640
+ if (isNodeOfType(a, "PrivateIdentifier") && isNodeOfType(b, "PrivateIdentifier")) return a.name === b.name;
7312
7641
  if (isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal")) return a.value === b.value;
7313
7642
  if (isNodeOfType(a, "MemberExpression") && isNodeOfType(b, "MemberExpression")) {
7314
7643
  if (a.computed !== b.computed) return false;
@@ -8364,17 +8693,6 @@ const createMethodMutationAnalysis = (context) => {
8364
8693
  //#region src/plugin/utils/is-member-property.ts
8365
8694
  const isMemberProperty = (node, propertyName) => Boolean(node && isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && node.property.name === propertyName);
8366
8695
  //#endregion
8367
- //#region src/plugin/utils/collect-function-return-statements.ts
8368
- const collectFunctionReturnStatements = (functionNode) => {
8369
- if (!isFunctionLike$1(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return [];
8370
- const returnStatements = [];
8371
- walkAst(functionNode.body, (node) => {
8372
- if (node !== functionNode.body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
8373
- if (isNodeOfType(node, "ReturnStatement")) returnStatements.push(node);
8374
- });
8375
- return returnStatements;
8376
- };
8377
- //#endregion
8378
8696
  //#region src/plugin/utils/statement-always-exits.ts
8379
8697
  const statementAlwaysExits = (statement) => {
8380
8698
  if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement")) return true;
@@ -8641,7 +8959,7 @@ const isReactNamespaceImport = (identifier, scopes) => {
8641
8959
  if (!symbol || !isImportedFromReact(symbol)) return false;
8642
8960
  return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
8643
8961
  };
8644
- const isReactNamespaceReceiver = (receiver, scopes, options) => {
8962
+ const isReactNamespaceReceiver$1 = (receiver, scopes, options) => {
8645
8963
  if (!isNodeOfType(receiver, "Identifier")) return false;
8646
8964
  if (isReactNamespaceImport(receiver, scopes)) return true;
8647
8965
  return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
@@ -8654,7 +8972,7 @@ const isDestructuredReactApiBinding = (identifier, apiNames, scopes, options) =>
8654
8972
  for (const property of pattern.properties) {
8655
8973
  if (!isNodeOfType(property, "Property") || property.value !== symbol.bindingIdentifier) continue;
8656
8974
  const propertyName = getStaticPropertyKeyName(property);
8657
- return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver(stripParenExpression(symbol.initializer), scopes, options));
8975
+ return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver$1(stripParenExpression(symbol.initializer), scopes, options));
8658
8976
  }
8659
8977
  return false;
8660
8978
  };
@@ -8678,7 +8996,7 @@ const isReactApiCallee = (rawCallee, apiNames, scopes, options, visitedSymbolIds
8678
8996
  return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
8679
8997
  }
8680
8998
  if (!isNodeOfType(callee, "MemberExpression") || !includesApiName(apiNames, getStaticPropertyName(callee) ?? "")) return false;
8681
- return isReactNamespaceReceiver(stripParenExpression(callee.object), scopes, options);
8999
+ return isReactNamespaceReceiver$1(stripParenExpression(callee.object), scopes, options);
8682
9000
  };
8683
9001
  //#endregion
8684
9002
  //#region src/plugin/utils/is-proven-browser-api-receiver.ts
@@ -9083,119 +9401,6 @@ const findReExportTargetsForName = (programRoot, exportedName) => {
9083
9401
  return exportAllTargets;
9084
9402
  };
9085
9403
  //#endregion
9086
- //#region src/plugin/utils/attach-parent-references.ts
9087
- const attachParentReferences = (root) => {
9088
- const visit = (node, parent) => {
9089
- const writableNode = node;
9090
- writableNode.parent = parent;
9091
- const nodeRecord = node;
9092
- for (const key of Object.keys(nodeRecord)) {
9093
- if (key === "parent") continue;
9094
- const child = nodeRecord[key];
9095
- if (Array.isArray(child)) {
9096
- for (const item of child) if (isAstNode(item)) visit(item, node);
9097
- } else if (isAstNode(child)) visit(child, node);
9098
- }
9099
- };
9100
- visit(root, null);
9101
- };
9102
- //#endregion
9103
- //#region src/plugin/utils/cross-file-probe-recorder.ts
9104
- let activeProbeTrace = null;
9105
- const recordExistenceProbe = (absolutePath) => {
9106
- activeProbeTrace?.existencePaths.add(absolutePath);
9107
- };
9108
- const recordContentProbe = (absolutePath) => {
9109
- activeProbeTrace?.contentPaths.add(absolutePath);
9110
- };
9111
- const isProbeRecorderActive = () => activeProbeTrace !== null;
9112
- const collectCrossFileProbes = (collect) => {
9113
- const previousTrace = activeProbeTrace;
9114
- const trace = {
9115
- existencePaths: /* @__PURE__ */ new Set(),
9116
- contentPaths: /* @__PURE__ */ new Set()
9117
- };
9118
- activeProbeTrace = trace;
9119
- try {
9120
- collect();
9121
- } finally {
9122
- activeProbeTrace = previousTrace;
9123
- }
9124
- return trace;
9125
- };
9126
- //#endregion
9127
- //#region src/plugin/utils/parse-source-file.ts
9128
- const FILENAME_TO_LANG = {
9129
- ".ts": "ts",
9130
- ".tsx": "tsx",
9131
- ".js": "js",
9132
- ".jsx": "jsx",
9133
- ".mjs": "js",
9134
- ".cjs": "js",
9135
- ".mts": "ts",
9136
- ".cts": "ts"
9137
- };
9138
- const resolveLang = (filename) => {
9139
- return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
9140
- };
9141
- const parseSourceText = ({ filename, sourceText, shouldAttachParentReferences = true }) => {
9142
- try {
9143
- const result = parseSync(filename, sourceText, {
9144
- astType: "ts",
9145
- lang: resolveLang(filename)
9146
- });
9147
- if (result.errors.some((parseError) => parseError.severity === "Error")) return null;
9148
- const parsedProgram = result.program;
9149
- if (shouldAttachParentReferences) attachParentReferences(parsedProgram);
9150
- return parsedProgram;
9151
- } catch {
9152
- return null;
9153
- }
9154
- };
9155
- const parseCache = /* @__PURE__ */ new Map();
9156
- const parseSourceFile = (absoluteFilePath) => {
9157
- if (!(absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts"))) recordContentProbe(absoluteFilePath);
9158
- let fileStat;
9159
- try {
9160
- fileStat = fs.statSync(absoluteFilePath);
9161
- } catch {
9162
- return null;
9163
- }
9164
- if (!fileStat.isFile()) return null;
9165
- if (fileStat.size > 2e6) return null;
9166
- const cached = parseCache.get(absoluteFilePath);
9167
- if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
9168
- if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
9169
- parseCache.set(absoluteFilePath, {
9170
- mtimeMs: fileStat.mtimeMs,
9171
- size: fileStat.size,
9172
- program: null
9173
- });
9174
- return null;
9175
- }
9176
- let sourceText;
9177
- try {
9178
- sourceText = fs.readFileSync(absoluteFilePath, "utf8");
9179
- } catch {
9180
- parseCache.set(absoluteFilePath, {
9181
- mtimeMs: fileStat.mtimeMs,
9182
- size: fileStat.size,
9183
- program: null
9184
- });
9185
- return null;
9186
- }
9187
- const parsedProgram = parseSourceText({
9188
- filename: absoluteFilePath,
9189
- sourceText
9190
- });
9191
- parseCache.set(absoluteFilePath, {
9192
- mtimeMs: fileStat.mtimeMs,
9193
- size: fileStat.size,
9194
- program: parsedProgram
9195
- });
9196
- return parsedProgram;
9197
- };
9198
- //#endregion
9199
9404
  //#region src/plugin/utils/resolve-relative-import-path.ts
9200
9405
  const MODULE_FILE_EXTENSIONS = [
9201
9406
  ".ts",
@@ -14662,6 +14867,29 @@ const getReleaseVerbName = (node) => {
14662
14867
  }
14663
14868
  return null;
14664
14869
  };
14870
+ const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) => {
14871
+ const releaseFunction = findEnclosingFunction$1(releaseReceiver);
14872
+ const usageFunction = findEnclosingFunction$1(usage.node);
14873
+ if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction) || !hasReactRefCurrentOrigin(releaseReceiver, context.scopes)) return false;
14874
+ const controllerKey = getListenerAbortControllerKey(usage, context);
14875
+ const refCurrentKey = resolveExpressionKey(releaseReceiver, context);
14876
+ if (controllerKey === null || refCurrentKey === null) return false;
14877
+ const usageFunctionBody = usageFunction.body;
14878
+ const previousAbortCalls = [];
14879
+ const ownershipAssignments = [];
14880
+ walkAst(usageFunctionBody, (child) => {
14881
+ if (child !== usageFunctionBody && isFunctionLike$1(child)) return false;
14882
+ if (isNodeOfType(child, "AssignmentExpression") && resolveExpressionKey(child.left, context) === refCurrentKey && resolveExpressionKey(child.right, context) === controllerKey) {
14883
+ ownershipAssignments.push(child);
14884
+ return;
14885
+ }
14886
+ if (!isNodeOfType(child, "CallExpression")) return;
14887
+ const childCallee = isNodeOfType(child.callee, "ChainExpression") ? child.callee.expression : stripParenExpression(child.callee);
14888
+ if (isNodeOfType(childCallee, "MemberExpression") && !childCallee.computed && isNodeOfType(childCallee.property, "Identifier") && childCallee.property.name === "abort" && resolveExpressionKey(childCallee.object, context) === refCurrentKey) previousAbortCalls.push(child);
14889
+ });
14890
+ const safeOwnershipAssignments = ownershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, previousAbortCalls, usageFunction, context));
14891
+ return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
14892
+ };
14665
14893
  const doesReleaseCallMatchUsage = (node, usage, context) => {
14666
14894
  const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
14667
14895
  if (!isNodeOfType(callNode, "CallExpression")) return false;
@@ -14681,6 +14909,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
14681
14909
  if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
14682
14910
  if (usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName))) return true;
14683
14911
  if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
14912
+ if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
14684
14913
  if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
14685
14914
  const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
14686
14915
  if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
@@ -14714,9 +14943,12 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
14714
14943
  return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
14715
14944
  }
14716
14945
  if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
14717
- const releaseHandler = callNode.arguments?.[1];
14946
+ const usesUnaryListenerSignature = usage.registrationVerbName === "addListener" && isNodeOfType(usage.node, "CallExpression") && usage.node.arguments?.length === 1 && callNode.arguments?.length === 1;
14947
+ const releaseHandler = usesUnaryListenerSignature ? callNode.arguments?.[0] : callNode.arguments?.[1];
14718
14948
  if (!releaseHandler) return releaseVerbName === "off";
14719
- return usage.handlerKey !== null && resolveExpressionKey(releaseHandler, context) === usage.handlerKey;
14949
+ const expectedHandlerKey = usesUnaryListenerSignature ? usage.eventKey : usage.handlerKey;
14950
+ const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[usesUnaryListenerSignature ? 0 : 1] : null;
14951
+ return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey || registrationHandler !== null && resolveStableValue(releaseHandler, context) === resolveStableValue(registrationHandler, context);
14720
14952
  }
14721
14953
  if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
14722
14954
  return true;
@@ -14729,8 +14961,7 @@ const isReturnedEffectCleanupFunction = (functionNode) => {
14729
14961
  currentNode = parentNode;
14730
14962
  parentNode = currentNode.parent;
14731
14963
  }
14732
- if (!isNodeOfType(parentNode, "ReturnStatement") || parentNode.argument !== currentNode) return false;
14733
- const effectCallback = findEnclosingFunction$1(parentNode);
14964
+ const effectCallback = isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode ? findEnclosingFunction$1(parentNode) : isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode ? parentNode : null;
14734
14965
  const effectCall = effectCallback?.parent;
14735
14966
  return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
14736
14967
  };
@@ -14742,13 +14973,188 @@ const isPotentiallyReachableFunction = (functionNode, context) => {
14742
14973
  if (!symbol) return false;
14743
14974
  return symbol.references.some((reference) => findEnclosingFunction$1(reference.identifier) !== functionNode);
14744
14975
  };
14976
+ const isJsxRefAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "ref";
14977
+ const isFunctionForwardedToReactRef = (functionNode, context) => {
14978
+ const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
14979
+ if (!bindingIdentifier) return false;
14980
+ const symbol = context.scopes.symbolFor(bindingIdentifier);
14981
+ if (!symbol) return false;
14982
+ return symbol.references.some((reference) => {
14983
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
14984
+ const expressionContainer = referenceRoot.parent;
14985
+ return Boolean(isNodeOfType(expressionContainer, "JSXExpressionContainer") && expressionContainer.expression === referenceRoot && isJsxRefAttribute(expressionContainer.parent));
14986
+ });
14987
+ };
14988
+ const findRetainedDisposerStorages = (disposerFunction, usage, context) => {
14989
+ if (!isFunctionLike$1(disposerFunction) || disposerFunction.async || disposerFunction.generator) return [];
14990
+ const usageFunction = findEnclosingFunction$1(usage.node);
14991
+ if (!usageFunction || !isFunctionLike$1(usageFunction)) return [];
14992
+ const assignments = /* @__PURE__ */ new Map();
14993
+ const collectAssignment = (expression) => {
14994
+ const expressionRoot = findTransparentExpressionRoot(expression);
14995
+ const assignment = expressionRoot.parent;
14996
+ if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.right !== expressionRoot) return;
14997
+ const refSymbol = resolveReactRefSymbol(stripParenExpression(assignment.left), context.scopes);
14998
+ const refCurrentKey = resolveExpressionKey(assignment.left, context);
14999
+ const retainedFunction = findEnclosingFunction$1(assignment);
15000
+ const assignmentStart = getRangeStart(assignment);
15001
+ if (!refSymbol || !refCurrentKey || !retainedFunction || retainedFunction !== usageFunction || assignmentStart === null) return;
15002
+ assignments.set(assignmentStart, {
15003
+ assignmentNode: assignment,
15004
+ refCurrentKey,
15005
+ retainedFunction
15006
+ });
15007
+ };
15008
+ collectAssignment(disposerFunction);
15009
+ const bindingIdentifier = getFunctionBindingIdentifier$1(disposerFunction);
15010
+ const symbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
15011
+ for (const reference of symbol?.references ?? []) collectAssignment(reference.identifier);
15012
+ walkAst(usageFunction.body, (child) => {
15013
+ if (child !== usageFunction.body && isFunctionLike$1(child)) return false;
15014
+ if (isNodeOfType(child, "AssignmentExpression") && resolveStableValue(child.right, context) === disposerFunction) collectAssignment(child.right);
15015
+ });
15016
+ return [...assignments.values()];
15017
+ };
15018
+ const isRetainedDisposerStorageEstablished = (storage, usage, context) => doMatchingNodesCoverEveryPathBeforeUsage(usage.node, [storage.assignmentNode], storage.retainedFunction, context) || doMatchingNodesCoverEveryPathAfterUsage(usage.node, [storage.assignmentNode], context);
15019
+ const hasUnsafeRetainedDisposerOverwrite = (storage, usage, context) => {
15020
+ let hasUnsafeOverwrite = false;
15021
+ walkAst(storage.retainedFunction.body, (child) => {
15022
+ if (hasUnsafeOverwrite) return false;
15023
+ if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
15024
+ if (!isNodeOfType(child, "AssignmentExpression") || child === storage.assignmentNode || resolveExpressionKey(child.left, context) !== storage.refCurrentKey || !canNodeReachLaterNodeWithinFunction(usage.node, child, storage.retainedFunction, context)) return;
15025
+ const storedValue = resolveStableValue(child.right, context);
15026
+ if (!storedValue || !isFunctionLike$1(storedValue) || !doesCleanupFunctionReleaseUsage(storedValue, usage, context)) {
15027
+ hasUnsafeOverwrite = true;
15028
+ return false;
15029
+ }
15030
+ });
15031
+ return hasUnsafeOverwrite;
15032
+ };
15033
+ const hasEffectCleanupInvocation = (storage, usage, context) => {
15034
+ const componentFunction = findEnclosingFunction$1(storage.retainedFunction);
15035
+ if (!componentFunction || !isFunctionLike$1(componentFunction)) return false;
15036
+ const cleanupFunctionInvokesRef = (cleanupFunction) => {
15037
+ if (!isFunctionLike$1(cleanupFunction)) return false;
15038
+ let didFindCleanupCall = false;
15039
+ walkAst(cleanupFunction.body, (child) => {
15040
+ if (didFindCleanupCall) return false;
15041
+ if (child !== cleanupFunction.body && isFunctionLike$1(child)) return false;
15042
+ if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) {
15043
+ const callRoot = findTransparentExpressionRoot(child);
15044
+ const callStatement = callRoot.parent;
15045
+ const isDirectBlockStatement = isNodeOfType(cleanupFunction.body, "BlockStatement") && isNodeOfType(callStatement, "ExpressionStatement") && callStatement.parent === cleanupFunction.body;
15046
+ const isConciseBody = cleanupFunction.body === callRoot;
15047
+ if ((isDirectBlockStatement || isConciseBody) && !hasUnprovenReturnBeforeRefOwnedRelease(cleanupFunction, child, storage.refCurrentKey, context)) {
15048
+ didFindCleanupCall = true;
15049
+ return false;
15050
+ }
15051
+ }
15052
+ });
15053
+ return didFindCleanupCall;
15054
+ };
15055
+ const effectReturnsCleanup = (effectCallback) => {
15056
+ if (!isFunctionLike$1(effectCallback)) return false;
15057
+ if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
15058
+ const cleanupFunction = resolveRefOwnedCleanupFunction(effectCallback.body, context);
15059
+ return Boolean(cleanupFunction && cleanupFunctionInvokesRef(cleanupFunction));
15060
+ }
15061
+ const matchingReturns = [];
15062
+ walkInsideStatementBlocks(effectCallback.body, (child) => {
15063
+ if (!isNodeOfType(child, "ReturnStatement") || !child.argument) return;
15064
+ const cleanupFunction = resolveRefOwnedCleanupFunction(child.argument, context);
15065
+ if (!cleanupFunction || !cleanupFunctionInvokesRef(cleanupFunction)) return;
15066
+ matchingReturns.push(child);
15067
+ });
15068
+ return doMatchingNodesCoverEveryPathFromFunctionEntry(effectCallback, matchingReturns, context);
15069
+ };
15070
+ let didFindInvocation = false;
15071
+ walkAst(componentFunction.body, (child) => {
15072
+ if (didFindInvocation) return false;
15073
+ if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction || !isReactApiCall(child, "useEffect", context.scopes)) return;
15074
+ const effectCallback = getEffectCallback(child);
15075
+ if (effectCallback && effectReturnsCleanup(effectCallback)) {
15076
+ didFindInvocation = true;
15077
+ return false;
15078
+ }
15079
+ });
15080
+ return didFindInvocation;
15081
+ };
15082
+ const hasCallbackRefReplacementInvocation = (storage, usage, context) => {
15083
+ const isReturnedCallbackRefShape = () => {
15084
+ if (!isFunctionLike$1(storage.retainedFunction)) return false;
15085
+ const callbackCall = findTransparentExpressionRoot(storage.retainedFunction).parent;
15086
+ if (!isNodeOfType(callbackCall, "CallExpression") || !isReactApiCall(callbackCall, "useCallback", context.scopes)) return false;
15087
+ const nodeParameter = storage.retainedFunction.params?.[0];
15088
+ const nodeParameterKey = resolveExpressionKey(nodeParameter, context);
15089
+ if (!nodeParameterKey || usage.receiverKey !== nodeParameterKey) return false;
15090
+ const bindingIdentifier = getFunctionBindingIdentifier$1(storage.retainedFunction);
15091
+ const symbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
15092
+ if (!Boolean(symbol?.references.some((reference) => {
15093
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
15094
+ const property = referenceRoot.parent;
15095
+ if (!isNodeOfType(property, "Property") || property.value !== referenceRoot || !isNodeOfType(property.parent, "ObjectExpression")) return false;
15096
+ const returnedObject = findTransparentExpressionRoot(property.parent);
15097
+ const returnStatement = returnedObject.parent;
15098
+ if (!isNodeOfType(returnStatement, "ReturnStatement") || returnStatement.argument !== returnedObject) return false;
15099
+ const hookFunction = findEnclosingFunction$1(returnStatement);
15100
+ return Boolean(hookFunction && getFunctionBindingIdentifier$1(hookFunction)?.name.startsWith("use"));
15101
+ }))) return false;
15102
+ const usageStart = getRangeStart(usage.node);
15103
+ if (usageStart === null) return false;
15104
+ let hasNullExit = false;
15105
+ walkAst(storage.retainedFunction.body, (child) => {
15106
+ if (hasNullExit) return false;
15107
+ if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
15108
+ if (!isNodeOfType(child, "IfStatement") || (getRangeStart(child) ?? usageStart) >= usageStart) return;
15109
+ const test = stripParenExpression(child.test);
15110
+ if (!isNodeOfType(test, "UnaryExpression") || test.operator !== "!" || resolveExpressionKey(test.argument, context) !== nodeParameterKey) return;
15111
+ const consequent = child.consequent;
15112
+ hasNullExit = isNodeOfType(consequent, "ReturnStatement") || isNodeOfType(consequent, "BlockStatement") && consequent.body.some((statement) => isNodeOfType(statement, "ReturnStatement"));
15113
+ if (hasNullExit) return false;
15114
+ });
15115
+ return hasNullExit;
15116
+ };
15117
+ if (!isFunctionForwardedToReactRef(storage.retainedFunction, context) && !isReturnedCallbackRefShape()) return false;
15118
+ const cleanupCalls = [];
15119
+ walkAst(storage.retainedFunction.body, (child) => {
15120
+ if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
15121
+ if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) cleanupCalls.push(child);
15122
+ });
15123
+ return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, cleanupCalls, storage.retainedFunction, context);
15124
+ };
15125
+ const isRetainedDisposerRefRelease = (releaseNode, usage, context) => {
15126
+ const disposerFunction = findEnclosingFunction$1(releaseNode);
15127
+ if (!disposerFunction) return false;
15128
+ return findRetainedDisposerStorages(disposerFunction, usage, context).some((storage) => isRetainedDisposerStorageEstablished(storage, usage, context) && !hasUnsafeRetainedDisposerOverwrite(storage, usage, context) && (hasEffectCleanupInvocation(storage, usage, context) || hasCallbackRefReplacementInvocation(storage, usage, context)));
15129
+ };
15130
+ const isSelfReleasingListenerRelease = (releaseNode, releaseFunction, usage, context) => {
15131
+ if (usage.kind !== "subscribe" || usage.registrationVerbName !== "addEventListener" || usage.receiverKey === null || usage.eventKey === null || !isNodeOfType(usage.node, "CallExpression") || !isFunctionLike$1(releaseFunction) || releaseFunction.async || releaseFunction.generator || !isNodeOfType(releaseFunction.body, "BlockStatement") || !doMatchingNodesCoverEveryPathFromFunctionEntry(releaseFunction, [releaseNode], context)) return false;
15132
+ const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
15133
+ const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
15134
+ if (!isNodeOfType(releaseCall, "CallExpression")) return false;
15135
+ const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
15136
+ if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
15137
+ const ownerFunction = findEnclosingFunction$1(releaseFunction);
15138
+ if (!ownerFunction || !isFunctionLike$1(ownerFunction)) return false;
15139
+ const triggerRegistrations = [];
15140
+ walkAst(ownerFunction.body, (child) => {
15141
+ if (child !== ownerFunction.body && isFunctionLike$1(child)) return false;
15142
+ if (!isNodeOfType(child, "CallExpression")) return;
15143
+ const registrationDetails = getCallRegistrationDetails(child, context);
15144
+ if (registrationDetails.registrationVerbName === "addEventListener" && registrationDetails.receiverKey === usage.receiverKey && resolveStableValue(child.arguments?.[1], context) === releaseFunction) triggerRegistrations.push(child);
15145
+ });
15146
+ if (triggerRegistrations.some((triggerRegistration) => triggerRegistration === usage.node)) return true;
15147
+ return doMatchingNodesCoverEveryPathAfterUsage(usage.node, triggerRegistrations, context) || doMatchingNodesCoverEveryPathBeforeUsage(usage.node, triggerRegistrations, ownerFunction, context);
15148
+ };
14745
15149
  const isReleaseReachableForUsage = (releaseNode, usage, context) => {
14746
15150
  if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
14747
15151
  const releaseFunction = findEnclosingFunction$1(releaseNode);
14748
15152
  if (!releaseFunction) return true;
14749
15153
  if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
15154
+ if (isRetainedDisposerRefRelease(releaseNode, usage, context)) return true;
14750
15155
  const usageFunction = findEnclosingFunction$1(usage.node);
14751
15156
  if (usageFunction && isFunctionLike$1(usageFunction) && getAssignedReactRefSymbol(usageFunction, context) && isCleanupFunctionReferencedByReturn(usageFunction, releaseFunction, context)) return isReactRefCallbackCleanupOwnedByEffect(usageFunction, releaseFunction, usage, context);
15157
+ if (isSelfReleasingListenerRelease(releaseNode, releaseFunction, usage, context)) return true;
14752
15158
  return isPotentiallyReachableFunction(releaseFunction, context);
14753
15159
  };
14754
15160
  const fileContainsReleaseForUsage = (usage, context) => {
@@ -15016,6 +15422,11 @@ const doesResourceResultEscape = (resourceNode, allowReturnedResourceEscape, all
15016
15422
  parentNode = currentNode.parent;
15017
15423
  continue;
15018
15424
  }
15425
+ if (isNodeOfType(parentNode, "ConditionalExpression") && (parentNode.consequent === currentNode || parentNode.alternate === currentNode) || isNodeOfType(parentNode, "LogicalExpression") && (parentNode.right === currentNode || parentNode.left === currentNode && parentNode.operator !== "&&")) {
15426
+ currentNode = parentNode;
15427
+ parentNode = currentNode.parent;
15428
+ continue;
15429
+ }
15019
15430
  if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode && isNodeOfType(parentNode.id, "Identifier") && isNodeOfType(parentNode.parent, "VariableDeclaration") && parentNode.parent.kind === "const") {
15020
15431
  const ownerFunction = findEnclosingFunction$1(resourceNode);
15021
15432
  const resourceSymbol = context.scopes.symbolFor(parentNode.id);
@@ -16877,7 +17288,7 @@ const collectCaptureDepKeys = (callback, scopes, declaredExactBindingKeys, allow
16877
17288
  keys.add(depKey);
16878
17289
  continue;
16879
17290
  }
16880
- const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
17291
+ const identitySourceKeys = resolvePureCalledFunctionSourceKeys(reference, symbol, scopes) ?? resolveRenderDerivedMutableSourceKeys(reference, symbol, scopes) ?? resolveReactiveIdentitySourceKeys(symbol, scopes);
16881
17292
  if (identitySourceKeys) {
16882
17293
  if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
16883
17294
  for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
@@ -16960,6 +17371,161 @@ const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
16960
17371
  if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
16961
17372
  return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
16962
17373
  };
17374
+ const isPureDerivedExpression = (expression) => {
17375
+ const candidate = unwrapExpression$3(expression);
17376
+ if (isNodeOfType(candidate, "Literal") || isNodeOfType(candidate, "Identifier")) return true;
17377
+ if (isNodeOfType(candidate, "MemberExpression")) return isPureDerivedExpression(candidate.object) && (!candidate.computed || isPureDerivedExpression(candidate.property));
17378
+ if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return isPureDerivedExpression(candidate.left) && isPureDerivedExpression(candidate.right);
17379
+ if (isNodeOfType(candidate, "UnaryExpression")) return candidate.operator !== "delete" && isPureDerivedExpression(candidate.argument);
17380
+ if (isNodeOfType(candidate, "ConditionalExpression")) return isPureDerivedExpression(candidate.test) && isPureDerivedExpression(candidate.consequent) && isPureDerivedExpression(candidate.alternate);
17381
+ if (isNodeOfType(candidate, "TemplateLiteral")) return candidate.expressions.every((nestedExpression) => isPureDerivedExpression(nestedExpression));
17382
+ return false;
17383
+ };
17384
+ const isPureDerivedStatement = (statement) => {
17385
+ if (isNodeOfType(statement, "BlockStatement")) return statement.body.every((nestedStatement) => isPureDerivedStatement(nestedStatement));
17386
+ if (isNodeOfType(statement, "ReturnStatement")) return !statement.argument || isPureDerivedExpression(statement.argument);
17387
+ if (isNodeOfType(statement, "IfStatement")) return isPureDerivedExpression(statement.test) && isPureDerivedStatement(statement.consequent) && (!statement.alternate || isPureDerivedStatement(statement.alternate));
17388
+ return false;
17389
+ };
17390
+ const isPureDerivedFunction = (functionNode) => {
17391
+ if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return false;
17392
+ if (functionNode.async || functionNode.generator) return false;
17393
+ return isNodeOfType(functionNode.body, "BlockStatement") ? isPureDerivedStatement(functionNode.body) : isPureDerivedExpression(functionNode.body);
17394
+ };
17395
+ const resolvePureCalledFunctionSourceKeys = (reference, symbol, scopes) => {
17396
+ if (symbol.references.some((symbolReference) => symbolReference.flag !== "read")) return null;
17397
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
17398
+ const callExpression = referenceRoot.parent;
17399
+ if (!isNodeOfType(callExpression, "CallExpression") || callExpression.callee !== referenceRoot) return null;
17400
+ const functionNode = getFunctionValueNode(symbol);
17401
+ if (!functionNode || !isPureDerivedFunction(functionNode)) return null;
17402
+ const sourceKeys = /* @__PURE__ */ new Set();
17403
+ for (const capturedReference of closureCaptures(functionNode, scopes)) {
17404
+ const capturedSymbol = capturedReference.resolvedSymbol;
17405
+ if (!capturedSymbol || capturedSymbol.id === symbol.id) continue;
17406
+ if (isOutsideAllFunctions(capturedSymbol) || symbolHasStableValue(capturedSymbol, scopes)) continue;
17407
+ const capturedKey = computeDepKey(capturedReference);
17408
+ if (!capturedKey) return null;
17409
+ if (capturedKey === capturedSymbol.name) {
17410
+ const nestedSourceKeys = resolveReactiveIdentitySourceKeys(capturedSymbol, scopes);
17411
+ if (nestedSourceKeys) {
17412
+ for (const nestedSourceKey of nestedSourceKeys) sourceKeys.add(nestedSourceKey);
17413
+ continue;
17414
+ }
17415
+ }
17416
+ sourceKeys.add(capturedKey);
17417
+ }
17418
+ return sourceKeys.size > 0 ? sourceKeys : null;
17419
+ };
17420
+ const mergeDerivedExpressionSourceKeys = (expressions, scopes, visitedSymbolIds) => {
17421
+ const sourceKeys = /* @__PURE__ */ new Set();
17422
+ for (const expression of expressions) {
17423
+ const expressionSourceKeys = resolveDerivedExpressionSourceKeys(expression, scopes, visitedSymbolIds);
17424
+ if (!expressionSourceKeys) return null;
17425
+ for (const expressionSourceKey of expressionSourceKeys) sourceKeys.add(expressionSourceKey);
17426
+ }
17427
+ return sourceKeys;
17428
+ };
17429
+ const resolveDerivedExpressionSourceKeys = (expression, scopes, visitedSymbolIds) => {
17430
+ const candidate = unwrapExpression$3(expression);
17431
+ if (isNodeOfType(candidate, "Literal")) return /* @__PURE__ */ new Set();
17432
+ if (isNodeOfType(candidate, "Identifier")) {
17433
+ if (scopes.isGlobalReference(candidate)) return /* @__PURE__ */ new Set();
17434
+ const sourceSymbol = scopes.symbolFor(candidate);
17435
+ if (!sourceSymbol) return null;
17436
+ if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
17437
+ if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((sourceReference) => sourceReference.flag === "read") && !visitedSymbolIds.has(sourceSymbol.id)) {
17438
+ visitedSymbolIds.add(sourceSymbol.id);
17439
+ const sourceKeys = resolveDerivedExpressionSourceKeys(sourceSymbol.initializer, scopes, visitedSymbolIds);
17440
+ visitedSymbolIds.delete(sourceSymbol.id);
17441
+ if (sourceKeys) return sourceKeys;
17442
+ }
17443
+ return new Set([sourceSymbol.name]);
17444
+ }
17445
+ if (isNodeOfType(candidate, "MemberExpression")) {
17446
+ if (hasComputedMemberExpression(candidate)) return null;
17447
+ const sourceKey = stringifyMemberChain(candidate);
17448
+ const rootIdentifier = getMemberRootIdentifier(candidate);
17449
+ const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
17450
+ if (!sourceKey || !rootSymbol) return null;
17451
+ if (isOutsideAllFunctions(rootSymbol) || symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
17452
+ return new Set([sourceKey]);
17453
+ }
17454
+ if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return mergeDerivedExpressionSourceKeys([candidate.left, candidate.right], scopes, visitedSymbolIds);
17455
+ if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator !== "delete") return resolveDerivedExpressionSourceKeys(candidate.argument, scopes, visitedSymbolIds);
17456
+ if (isNodeOfType(candidate, "ConditionalExpression")) return mergeDerivedExpressionSourceKeys([
17457
+ candidate.test,
17458
+ candidate.consequent,
17459
+ candidate.alternate
17460
+ ], scopes, visitedSymbolIds);
17461
+ if (isNodeOfType(candidate, "TemplateLiteral")) return mergeDerivedExpressionSourceKeys(candidate.expressions, scopes, visitedSymbolIds);
17462
+ if (isNodeOfType(candidate, "NewExpression")) {
17463
+ const callee = unwrapExpression$3(candidate.callee);
17464
+ if (!isNodeOfType(callee, "Identifier") || callee.name !== "Error" || !scopes.isGlobalReference(callee)) return null;
17465
+ const argumentsToAnalyze = [];
17466
+ for (const argument of candidate.arguments) {
17467
+ if (!isAstNode(argument) || isNodeOfType(argument, "SpreadElement")) return null;
17468
+ argumentsToAnalyze.push(argument);
17469
+ }
17470
+ return mergeDerivedExpressionSourceKeys(argumentsToAnalyze, scopes, visitedSymbolIds);
17471
+ }
17472
+ return null;
17473
+ };
17474
+ const resolveWriteControlSourceKeys = (assignment, boundaryFunction, scopes) => {
17475
+ const sourceKeys = /* @__PURE__ */ new Set();
17476
+ let currentNode = assignment;
17477
+ while (currentNode.parent && currentNode.parent !== boundaryFunction) {
17478
+ const parentNode = currentNode.parent;
17479
+ if (isNodeOfType(parentNode, "IfStatement")) {
17480
+ if (parentNode.test === currentNode) return null;
17481
+ const testSourceKeys = resolveDerivedExpressionSourceKeys(parentNode.test, scopes, /* @__PURE__ */ new Set());
17482
+ if (!testSourceKeys) return null;
17483
+ for (const testSourceKey of testSourceKeys) sourceKeys.add(testSourceKey);
17484
+ } else if (!isNodeOfType(parentNode, "ExpressionStatement") && !isNodeOfType(parentNode, "BlockStatement")) return null;
17485
+ currentNode = parentNode;
17486
+ }
17487
+ return currentNode.parent === boundaryFunction ? sourceKeys : null;
17488
+ };
17489
+ const isReadOnlyInitialStateUse = (referenceNode, scopes) => {
17490
+ const referenceRoot = findTransparentExpressionRoot(referenceNode);
17491
+ const callExpression = referenceRoot.parent;
17492
+ return isNodeOfType(callExpression, "CallExpression") && callExpression.arguments.some((argument) => argument === referenceRoot) && isReactApiCall(callExpression, "useState", scopes, {
17493
+ allowGlobalReactNamespace: true,
17494
+ allowUnboundBareCalls: true,
17495
+ resolveNamedAliases: true
17496
+ });
17497
+ };
17498
+ const resolveRenderDerivedMutableSourceKeys = (capturedReference, symbol, scopes) => {
17499
+ if (symbol.kind !== "let" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
17500
+ const boundaryFunction = findEnclosingFunction$1(symbol.bindingIdentifier);
17501
+ if (!boundaryFunction) return null;
17502
+ const capturingFunction = findEnclosingFunction$1(capturedReference.identifier);
17503
+ if (!capturingFunction || capturingFunction === boundaryFunction) return null;
17504
+ const sourceKeys = /* @__PURE__ */ new Set();
17505
+ if (symbol.initializer) {
17506
+ const initializerSourceKeys = resolveDerivedExpressionSourceKeys(symbol.initializer, scopes, new Set([symbol.id]));
17507
+ if (!initializerSourceKeys) return null;
17508
+ for (const initializerSourceKey of initializerSourceKeys) sourceKeys.add(initializerSourceKey);
17509
+ }
17510
+ let writeCount = 0;
17511
+ for (const symbolReference of symbol.references) {
17512
+ if (symbolReference.flag === "read") {
17513
+ if (findEnclosingFunction$1(symbolReference.identifier) !== capturingFunction && !isReadOnlyInitialStateUse(symbolReference.identifier, scopes)) return null;
17514
+ continue;
17515
+ }
17516
+ if (symbolReference.flag !== "write") return null;
17517
+ const referenceRoot = findTransparentExpressionRoot(symbolReference.identifier);
17518
+ const assignment = referenceRoot.parent;
17519
+ if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== referenceRoot || findEnclosingFunction$1(referenceRoot) !== boundaryFunction) return null;
17520
+ const assignmentSourceKeys = resolveDerivedExpressionSourceKeys(assignment.right, scopes, new Set([symbol.id]));
17521
+ const controlSourceKeys = resolveWriteControlSourceKeys(assignment, boundaryFunction, scopes);
17522
+ if (!assignmentSourceKeys || !controlSourceKeys) return null;
17523
+ for (const assignmentSourceKey of assignmentSourceKeys) sourceKeys.add(assignmentSourceKey);
17524
+ for (const controlSourceKey of controlSourceKeys) sourceKeys.add(controlSourceKey);
17525
+ writeCount += 1;
17526
+ }
17527
+ return writeCount > 0 && sourceKeys.size > 0 ? sourceKeys : null;
17528
+ };
16963
17529
  const isUseCallbackResultDep = (node, scopes) => {
16964
17530
  const rootSymbol = getRootSymbol(node, scopes);
16965
17531
  const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
@@ -27251,7 +27817,11 @@ const mouseEventsHaveKeyEvents = defineRule({
27251
27817
  //#region src/plugin/utils/has-directive.ts
27252
27818
  const hasDirective = (programNode, directive) => {
27253
27819
  if (!isNodeOfType(programNode, "Program")) return false;
27254
- return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
27820
+ for (const statement of programNode.body) {
27821
+ if (!isNodeOfType(statement, "ExpressionStatement") || statement.directive === void 0) return false;
27822
+ if (statement.directive === directive) return true;
27823
+ }
27824
+ return false;
27255
27825
  };
27256
27826
  //#endregion
27257
27827
  //#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
@@ -29183,7 +29753,7 @@ const nextjsNoVercelOgImport = defineRule({
29183
29753
  //#endregion
29184
29754
  //#region src/plugin/rules/a11y/no-access-key.ts
29185
29755
  const MESSAGE$39 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
29186
- const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
29756
+ const isUndefinedIdentifier$1 = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
29187
29757
  const noAccessKey = defineRule({
29188
29758
  id: "no-access-key",
29189
29759
  title: "accessKey attribute used",
@@ -29208,7 +29778,7 @@ const noAccessKey = defineRule({
29208
29778
  if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
29209
29779
  const expression = attributeValue.expression;
29210
29780
  if (!expression || expression.type === "JSXEmptyExpression") return;
29211
- if (isUndefinedIdentifier(expression)) return;
29781
+ if (isUndefinedIdentifier$1(expression)) return;
29212
29782
  context.report({
29213
29783
  node: accessKey,
29214
29784
  message: MESSAGE$39
@@ -29973,6 +30543,12 @@ const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.som
29973
30543
  const importDeclaration = declarationNode.parent;
29974
30544
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
29975
30545
  }));
30546
+ const isReactNamespaceReceiver = (analysis, node) => {
30547
+ const receiver = stripParenExpression(node);
30548
+ if (!isNodeOfType(receiver, "Identifier")) return false;
30549
+ const namespaceReference = getRef(analysis, receiver);
30550
+ return namespaceReference?.resolved ? isReactNamespaceImportReference(namespaceReference) : receiver.name === "React";
30551
+ };
29976
30552
  const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
29977
30553
  if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
29978
30554
  const callee = stripParenExpression(declarator.init.callee);
@@ -29981,24 +30557,20 @@ const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
29981
30557
  if (!reference?.resolved) return callee.name === hookName;
29982
30558
  return isReactNamedImportReference(reference, hookName);
29983
30559
  }
29984
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
29985
- const namespaceReference = getRef(analysis, callee.object);
29986
- if (!namespaceReference?.resolved) return callee.object.name === "React";
29987
- return isReactNamespaceImportReference(namespaceReference);
30560
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
30561
+ return isReactNamespaceReceiver(analysis, callee.object);
29988
30562
  };
29989
30563
  const isHookCallee$1 = (analysis, node, hookName) => {
29990
30564
  if (!node) return false;
29991
30565
  if (isNodeOfType(node, "Identifier")) {
29992
30566
  if (node.name === hookName) return true;
29993
30567
  if (isReactNamedImportReference(getRef(analysis, node), hookName)) return true;
29994
- const parent = node.parent;
29995
- if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
30568
+ const receiverRoot = findTransparentExpressionRoot(node);
30569
+ const parent = receiverRoot.parent;
30570
+ if (parent && isNodeOfType(parent, "MemberExpression") && parent.object === receiverRoot && isReactNamespaceReceiver(analysis, node) && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
29996
30571
  return false;
29997
30572
  }
29998
- if (isNodeOfType(node, "MemberExpression")) {
29999
- const receiver = stripParenExpression(node.object);
30000
- return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
30001
- }
30573
+ if (isNodeOfType(node, "MemberExpression")) return isReactNamespaceReceiver(analysis, node.object) && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
30002
30574
  return false;
30003
30575
  };
30004
30576
  const isUseEffect = (node) => {
@@ -30422,7 +30994,88 @@ const isIndependentWriterIdentifier = (componentFunction, identifier, includeDef
30422
30994
  if (HANDLER_BINDING_NAME_PATTERN.test(bindingName)) return true;
30423
30995
  return isSetterWiredToJsxHandler(componentFunction, bindingName);
30424
30996
  };
30425
- const hasUserInputSetterWriter = (setterRef, effectNode, includeDeferredWriters = false) => {
30997
+ const isSynchronousFunction = (functionNode) => {
30998
+ const functionMetadata = functionNode;
30999
+ return functionMetadata.async !== true && functionMetadata.generator !== true;
31000
+ };
31001
+ const findBindingVariable = (analysis, bindingIdentifier) => {
31002
+ for (const scope of analysis.scopeManager.scopes) for (const variable of scope.variables) if (variable.identifiers.includes(bindingIdentifier)) return variable;
31003
+ return null;
31004
+ };
31005
+ const getImmutableFunctionVariable = (analysis, componentFunction, functionNode) => {
31006
+ if (!isSynchronousFunction(functionNode) || !isAstDescendant(functionNode, componentFunction)) return null;
31007
+ const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
31008
+ if (!bindingIdentifier) return null;
31009
+ const variable = findBindingVariable(analysis, bindingIdentifier);
31010
+ if (!variable || variable.defs.length !== 1 || variable.references.some((reference) => reference.isWrite() && !reference.init)) return null;
31011
+ const definition = variable.defs[0];
31012
+ if (definition.type === "FunctionName") return definition.node === functionNode ? variable : null;
31013
+ if (definition.type !== "Variable") return null;
31014
+ const declarator = definition.node;
31015
+ if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.parent, "VariableDeclaration") || declarator.parent.kind !== "const") return null;
31016
+ if (declarator.init === functionNode) return variable;
31017
+ if (isNodeOfType(declarator.init, "CallExpression") && declarator.init.arguments?.[0] === functionNode && isGenuineReactHookDeclarator(analysis, declarator, "useCallback")) return variable;
31018
+ return null;
31019
+ };
31020
+ const getJsxEventValueAttribute = (identifier) => {
31021
+ const expression = findTransparentExpressionRoot(identifier);
31022
+ const expressionContainer = expression.parent;
31023
+ if (!isNodeOfType(expressionContainer, "JSXExpressionContainer") || expressionContainer.expression !== expression) return null;
31024
+ const attribute = expressionContainer.parent;
31025
+ if (!isNodeOfType(attribute, "JSXAttribute")) return null;
31026
+ const attributeName = getJsxAttributeName(attribute.name);
31027
+ return attributeName && isEventHandlerName(attributeName) ? attribute : null;
31028
+ };
31029
+ const getInlineJsxEventCallbackAttribute = (callExpression) => {
31030
+ const callbackFunction = findEnclosingFunction$1(callExpression);
31031
+ if (!callbackFunction || !isSynchronousFunction(callbackFunction)) return null;
31032
+ return getJsxEventValueAttribute(callbackFunction);
31033
+ };
31034
+ const isReactHookDependencyReference = (identifier) => {
31035
+ const expression = findTransparentExpressionRoot(identifier);
31036
+ const dependencyArray = expression.parent;
31037
+ if (!isNodeOfType(dependencyArray, "ArrayExpression") || !(dependencyArray.elements ?? []).includes(expression)) return false;
31038
+ const hookCall = dependencyArray.parent;
31039
+ if (!isNodeOfType(hookCall, "CallExpression") || hookCall.arguments?.[1] !== dependencyArray) return false;
31040
+ const callee = hookCall.callee;
31041
+ if (isNodeOfType(callee, "Identifier")) return /^use[A-Z0-9]/.test(callee.name);
31042
+ return Boolean(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && /^use[A-Z0-9]/.test(callee.property.name));
31043
+ };
31044
+ const hasReachableJsxEventCallPath = (analysis, context, componentFunction, functionVariable, visitedVariables) => {
31045
+ if (visitedVariables.has(functionVariable)) return false;
31046
+ const nextVisitedVariables = new Set(visitedVariables).add(functionVariable);
31047
+ const callExpressions = [];
31048
+ let hasDirectJsxEventReference = false;
31049
+ for (const reference of functionVariable.references) {
31050
+ if (reference.init) continue;
31051
+ const identifier = reference.identifier;
31052
+ if (reference.isWrite()) return false;
31053
+ const jsxEventValueAttribute = getJsxEventValueAttribute(identifier);
31054
+ if (jsxEventValueAttribute) {
31055
+ if (isNodeReachableWithinFunction(jsxEventValueAttribute, context)) hasDirectJsxEventReference = true;
31056
+ continue;
31057
+ }
31058
+ if (isReactHookDependencyReference(identifier)) continue;
31059
+ const callExpression = getCallExpr(reference);
31060
+ if (!callExpression) return false;
31061
+ const jsxEventCallbackAttribute = getInlineJsxEventCallbackAttribute(callExpression);
31062
+ if (jsxEventCallbackAttribute) {
31063
+ if (isNodeReachableWithinFunction(callExpression, context) && isNodeReachableWithinFunction(jsxEventCallbackAttribute, context)) hasDirectJsxEventReference = true;
31064
+ continue;
31065
+ }
31066
+ callExpressions.push(callExpression);
31067
+ }
31068
+ if (hasDirectJsxEventReference) return true;
31069
+ for (const callExpression of callExpressions) {
31070
+ if (!isNodeReachableWithinFunction(callExpression, context)) continue;
31071
+ const callerFunction = findEnclosingFunction$1(callExpression);
31072
+ if (!callerFunction || callerFunction === componentFunction) continue;
31073
+ const callerVariable = getImmutableFunctionVariable(analysis, componentFunction, callerFunction);
31074
+ if (callerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, callerVariable, nextVisitedVariables)) return true;
31075
+ }
31076
+ return false;
31077
+ };
31078
+ const hasUserInputSetterWriter = (analysis, context, setterRef, effectNode, includeDeferredWriters = false) => {
30426
31079
  if (!setterRef.resolved) return false;
30427
31080
  const componentFunction = findEnclosingFunction$1(effectNode);
30428
31081
  if (!componentFunction) return false;
@@ -30431,6 +31084,11 @@ const hasUserInputSetterWriter = (setterRef, effectNode, includeDeferredWriters
30431
31084
  const identifier = reference.identifier;
30432
31085
  if (isAstDescendant(identifier, effectNode)) continue;
30433
31086
  if (isIndependentWriterIdentifier(componentFunction, identifier, includeDeferredWriters)) return true;
31087
+ if (!isNodeReachableWithinFunction(identifier, context)) continue;
31088
+ const writerFunction = findEnclosingFunction$1(identifier);
31089
+ if (!writerFunction || writerFunction === componentFunction) continue;
31090
+ const writerVariable = getImmutableFunctionVariable(analysis, componentFunction, writerFunction);
31091
+ if (writerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, writerVariable, /* @__PURE__ */ new Set())) return true;
30434
31092
  }
30435
31093
  return false;
30436
31094
  };
@@ -31320,7 +31978,7 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
31320
31978
  }
31321
31979
  return false;
31322
31980
  };
31323
- const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
31981
+ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFilename) => {
31324
31982
  const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
31325
31983
  if (frames.length === 0) return [];
31326
31984
  const effectHasCleanup = hasCleanup(analysis, effectNode);
@@ -31350,7 +32008,7 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
31350
32008
  for (const returnedExpression of returnedExpressions) mergeEvidence(valueEvidence, collectValueEvidence(analysis, returnedExpression, updaterFrame, remainingValueCallFrames));
31351
32009
  } else valueEvidence = collectValueEvidence(analysis, writtenValue, frame, remainingValueCallFrames);
31352
32010
  const sourceReferences = [...valueEvidence.sourceReferences].filter((sourceReference) => getUseStateDecl(analysis, sourceReference) !== stateDeclarator);
31353
- const hasIndependentWriter = hasUserInputSetterWriter(setterReference, effectNode, true);
32011
+ const hasIndependentWriter = hasUserInputSetterWriter(analysis, context, setterReference, effectNode, true);
31354
32012
  const doesMatchStateInitializer = matchesStateInitializer(analysis, callExpression, stateDeclarator);
31355
32013
  if (effectHasCleanup && (frame.isDeferred || valueEvidence.hasUnknownSource || valueEvidence.hasDeferredIntroducedValue || valueEvidence.readsExternalValue)) cleanupManagedStateDeclarators.add(stateDeclarator);
31356
32014
  const isRenderKnownCopy = sourceReferences.length > 0 && !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue && !hasIndependentWriter;
@@ -31391,7 +32049,7 @@ const noAdjustStateOnPropChange = defineRule({
31391
32049
  const dependencyReferences = getEffectDepsRefs(analysis, node);
31392
32050
  if (!dependencyReferences) return;
31393
32051
  if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
31394
- for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
32052
+ for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
31395
32053
  if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
31396
32054
  context.report({
31397
32055
  node: fact.callExpression,
@@ -34078,6 +34736,7 @@ const noChainStateUpdates = defineRule({
34078
34736
  id: "no-chain-state-updates",
34079
34737
  title: "State updates chained through effects",
34080
34738
  severity: "warn",
34739
+ disabledWhen: ["react:18"],
34081
34740
  tags: ["test-noise"],
34082
34741
  recommendation: "Set all the related state together in the event handler that starts it, instead of having one useEffect react to a state change and set more state. See https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations",
34083
34742
  create: (context) => ({ CallExpression(node) {
@@ -35964,7 +36623,7 @@ const noDerivedState = defineRule({
35964
36623
  if (!isUseEffect(node)) return;
35965
36624
  const analysis = getProgramAnalysis(node);
35966
36625
  if (!analysis) return;
35967
- for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
36626
+ for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
35968
36627
  if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
35969
36628
  reportStateWrite(fact.callExpression, fact.stateDeclarator);
35970
36629
  }
@@ -35984,7 +36643,7 @@ const noDerivedStateEffect = defineRule({
35984
36643
  if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
35985
36644
  const analysis = getProgramAnalysis(node);
35986
36645
  if (!analysis) return;
35987
- if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
36646
+ if (!collectEffectStateWriteFacts(analysis, context, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
35988
36647
  context.report({
35989
36648
  node,
35990
36649
  message: "You pay an extra render for state you can derive from other values."
@@ -36460,9 +37119,20 @@ const noDidMountSetState = defineRule({
36460
37119
  }
36461
37120
  });
36462
37121
  //#endregion
37122
+ //#region src/plugin/utils/find-enclosing-class.ts
37123
+ const findEnclosingClass = (node) => {
37124
+ let ancestor = node.parent;
37125
+ while (ancestor) {
37126
+ if (isNodeOfType(ancestor, "ClassDeclaration") || isNodeOfType(ancestor, "ClassExpression")) return ancestor;
37127
+ ancestor = ancestor.parent ?? null;
37128
+ }
37129
+ return null;
37130
+ };
37131
+ //#endregion
36463
37132
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
36464
37133
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
36465
37134
  const MESSAGE$27 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
37135
+ const DIFFERENCE_OPERATORS = new Set(["!=", "!=="]);
36466
37136
  const EQUALITY_OPERATORS = new Set([
36467
37137
  "==",
36468
37138
  "===",
@@ -36474,6 +37144,8 @@ const FUNCTION_NODE_TYPES = new Set([
36474
37144
  "FunctionExpression",
36475
37145
  "ArrowFunctionExpression"
36476
37146
  ]);
37147
+ const CLASS_NODE_TYPES = new Set(["ClassDeclaration", "ClassExpression"]);
37148
+ const callbackRefFieldNamesByClass = /* @__PURE__ */ new WeakMap();
36477
37149
  const isLifecycleMethodFunction = (node) => {
36478
37150
  if (!FUNCTION_NODE_TYPES.has(node.type)) return false;
36479
37151
  const parent = node.parent;
@@ -36529,6 +37201,187 @@ const getStaticMemberName = (node) => {
36529
37201
  if (!isNodeOfType(node, "MemberExpression") || node.computed === true) return null;
36530
37202
  return isNodeOfType(node.property, "Identifier") ? node.property.name : null;
36531
37203
  };
37204
+ const getMemberIdentity = (property) => {
37205
+ const propertyName = getPropertyKeyName$2(property);
37206
+ if (propertyName !== void 0) return isNodeOfType(property, "PrivateIdentifier") ? `#${propertyName}` : propertyName;
37207
+ return isNodeOfType(property, "Literal") && typeof property.value === "string" ? property.value : null;
37208
+ };
37209
+ const collectPreviousSourcePaths = (pattern, domain, members, previousSourcePaths) => {
37210
+ if (!pattern) return;
37211
+ const unwrappedPattern = stripParenExpression(pattern);
37212
+ if (isNodeOfType(unwrappedPattern, "Identifier")) {
37213
+ previousSourcePaths.set(unwrappedPattern.name, {
37214
+ domain,
37215
+ members: [...members],
37216
+ source: "previous"
37217
+ });
37218
+ return;
37219
+ }
37220
+ if (isNodeOfType(unwrappedPattern, "AssignmentPattern")) {
37221
+ collectPreviousSourcePaths(unwrappedPattern.left, domain, members, previousSourcePaths);
37222
+ return;
37223
+ }
37224
+ if (!isNodeOfType(unwrappedPattern, "ObjectPattern")) return;
37225
+ for (const property of unwrappedPattern.properties) {
37226
+ if (!isNodeOfType(property, "Property")) continue;
37227
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
37228
+ if (!propertyName) continue;
37229
+ collectPreviousSourcePaths(property.value, domain, [...members, propertyName], previousSourcePaths);
37230
+ }
37231
+ };
37232
+ const getStateSourcePath = (node, previousSourcePaths) => {
37233
+ let currentNode = stripParenExpression(node);
37234
+ const members = [];
37235
+ while (isNodeOfType(currentNode, "MemberExpression")) {
37236
+ const memberName = getStaticMemberName(currentNode);
37237
+ if (!memberName) return null;
37238
+ members.unshift(memberName);
37239
+ currentNode = stripParenExpression(currentNode.object);
37240
+ }
37241
+ if (isNodeOfType(currentNode, "ThisExpression")) {
37242
+ const [domain, ...pathMembers] = members;
37243
+ if (domain !== "props" && domain !== "state") return null;
37244
+ return {
37245
+ domain,
37246
+ members: pathMembers,
37247
+ source: "current"
37248
+ };
37249
+ }
37250
+ if (!isNodeOfType(currentNode, "Identifier")) return null;
37251
+ const previousSourcePath = previousSourcePaths.get(currentNode.name);
37252
+ return previousSourcePath ? {
37253
+ ...previousSourcePath,
37254
+ members: [...previousSourcePath.members, ...members]
37255
+ } : null;
37256
+ };
37257
+ const haveMatchingStateSourcePaths = (left, right) => left.domain === right.domain && left.members.length === right.members.length && left.members.every((member, index) => member === right.members[index]);
37258
+ const collectConjunctiveStateSourceComparisons = (test, previousSourcePaths, comparisons) => {
37259
+ const expression = stripParenExpression(test);
37260
+ if (isNodeOfType(expression, "LogicalExpression") && expression.operator === "&&") {
37261
+ collectConjunctiveStateSourceComparisons(expression.left, previousSourcePaths, comparisons);
37262
+ collectConjunctiveStateSourceComparisons(expression.right, previousSourcePaths, comparisons);
37263
+ return;
37264
+ }
37265
+ if (!isNodeOfType(expression, "BinaryExpression") || !EQUALITY_OPERATORS.has(expression.operator)) return;
37266
+ const leftPath = getStateSourcePath(expression.left, previousSourcePaths);
37267
+ const rightPath = getStateSourcePath(expression.right, previousSourcePaths);
37268
+ if (Boolean(leftPath) === Boolean(rightPath)) return;
37269
+ const path = leftPath ?? rightPath;
37270
+ if (!path) return;
37271
+ comparisons.push({
37272
+ comparedValue: leftPath ? expression.right : expression.left,
37273
+ isDifference: DIFFERENCE_OPERATORS.has(expression.operator),
37274
+ path
37275
+ });
37276
+ };
37277
+ const isHistoricalToCurrentTransitionGuard = (test, previousSourcePaths) => {
37278
+ const expression = stripParenExpression(test);
37279
+ if (isNodeOfType(expression, "LogicalExpression") && expression.operator === "||") return isHistoricalToCurrentTransitionGuard(expression.left, previousSourcePaths) && isHistoricalToCurrentTransitionGuard(expression.right, previousSourcePaths);
37280
+ const comparisons = [];
37281
+ collectConjunctiveStateSourceComparisons(expression, previousSourcePaths, comparisons);
37282
+ return comparisons.some((comparison, index) => comparisons.slice(index + 1).some((candidate) => comparison.path.source !== candidate.path.source && comparison.isDifference !== candidate.isDifference && haveMatchingStateSourcePaths(comparison.path, candidate.path) && areExpressionsStructurallyEqual(comparison.comparedValue, candidate.comparedValue)));
37283
+ };
37284
+ const getThisFieldName = (node) => {
37285
+ const unwrappedNode = stripParenExpression(node);
37286
+ if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed === true || !isNodeOfType(stripParenExpression(unwrappedNode.object), "ThisExpression")) return null;
37287
+ return getMemberIdentity(unwrappedNode.property);
37288
+ };
37289
+ const isUndefinedIdentifier = (node) => {
37290
+ const unwrappedNode = stripParenExpression(node);
37291
+ return isNodeOfType(unwrappedNode, "Identifier") && unwrappedNode.name === "undefined";
37292
+ };
37293
+ const isDirectRefParameterValue = (node, parameterSymbolId, scopes) => {
37294
+ const unwrappedNode = stripParenExpression(node);
37295
+ if (isNodeOfType(unwrappedNode, "Identifier")) return scopes.symbolFor(unwrappedNode)?.id === parameterSymbolId;
37296
+ if (!isNodeOfType(unwrappedNode, "LogicalExpression") || unwrappedNode.operator !== "??") return false;
37297
+ const left = stripParenExpression(unwrappedNode.left);
37298
+ return isNodeOfType(left, "Identifier") && scopes.symbolFor(left)?.id === parameterSymbolId && isUndefinedIdentifier(unwrappedNode.right);
37299
+ };
37300
+ const getCallbackRefAssignedFields = (callback, scopes) => {
37301
+ const firstParameter = (callback.params ?? [])[0];
37302
+ if (!firstParameter) return /* @__PURE__ */ new Set();
37303
+ const parameterIdentifier = isNodeOfType(firstParameter, "AssignmentPattern") ? firstParameter.left : firstParameter;
37304
+ if (!isNodeOfType(parameterIdentifier, "Identifier")) return /* @__PURE__ */ new Set();
37305
+ const parameterSymbolId = scopes.symbolFor(parameterIdentifier)?.id;
37306
+ if (parameterSymbolId === void 0) return /* @__PURE__ */ new Set();
37307
+ const body = callback.body;
37308
+ if (!body) return /* @__PURE__ */ new Set();
37309
+ const assignedFieldNames = /* @__PURE__ */ new Set();
37310
+ walkAst(body, (node) => {
37311
+ if (node !== body && (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node) || CLASS_NODE_TYPES.has(node.type))) return false;
37312
+ const assignmentTarget = isNodeOfType(node, "AssignmentExpression") && node.left || isNodeOfType(node, "UpdateExpression") && node.argument || isNodeOfType(node, "UnaryExpression") && node.operator === "delete" && node.argument || null;
37313
+ if (!assignmentTarget) return;
37314
+ const fieldName = getThisFieldName(assignmentTarget);
37315
+ if (!fieldName) return;
37316
+ if (isNodeOfType(node, "AssignmentExpression") && node.operator === "=" && isDirectRefParameterValue(node.right, parameterSymbolId, scopes)) {
37317
+ assignedFieldNames.add(fieldName);
37318
+ return;
37319
+ }
37320
+ assignedFieldNames.delete(fieldName);
37321
+ });
37322
+ return assignedFieldNames;
37323
+ };
37324
+ const getClassMemberCallback = (classNode, memberName) => {
37325
+ const classBody = classNode.body?.body ?? [];
37326
+ for (const member of classBody) {
37327
+ if (!isNodeOfType(member, "MethodDefinition") && !isNodeOfType(member, "PropertyDefinition")) continue;
37328
+ if (member.static === true) continue;
37329
+ const key = member.key;
37330
+ if (getMemberIdentity(key) !== memberName) continue;
37331
+ const value = member.value;
37332
+ return value && FUNCTION_NODE_TYPES.has(value.type) ? value : null;
37333
+ }
37334
+ return null;
37335
+ };
37336
+ const collectCallbackRefFieldsFromExpression = (expression, classNode, fieldNames, scopes) => {
37337
+ const unwrappedExpression = stripParenExpression(expression);
37338
+ if (FUNCTION_NODE_TYPES.has(unwrappedExpression.type)) {
37339
+ for (const fieldName of getCallbackRefAssignedFields(unwrappedExpression, scopes)) fieldNames.add(fieldName);
37340
+ return;
37341
+ }
37342
+ const handlerName = getThisFieldName(unwrappedExpression);
37343
+ if (handlerName) {
37344
+ const callback = getClassMemberCallback(classNode, handlerName);
37345
+ if (callback) for (const fieldName of getCallbackRefAssignedFields(callback, scopes)) fieldNames.add(fieldName);
37346
+ return;
37347
+ }
37348
+ if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) {
37349
+ collectCallbackRefFieldsFromExpression(unwrappedExpression.consequent, classNode, fieldNames, scopes);
37350
+ collectCallbackRefFieldsFromExpression(unwrappedExpression.alternate, classNode, fieldNames, scopes);
37351
+ return;
37352
+ }
37353
+ if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
37354
+ if (unwrappedExpression.operator !== "&&") collectCallbackRefFieldsFromExpression(unwrappedExpression.left, classNode, fieldNames, scopes);
37355
+ collectCallbackRefFieldsFromExpression(unwrappedExpression.right, classNode, fieldNames, scopes);
37356
+ }
37357
+ };
37358
+ const getCallbackRefFieldNames = (classNode, scopes) => {
37359
+ if (!classNode) return /* @__PURE__ */ new Set();
37360
+ const cachedFieldNames = callbackRefFieldNamesByClass.get(classNode);
37361
+ if (cachedFieldNames) return cachedFieldNames;
37362
+ const fieldNames = /* @__PURE__ */ new Set();
37363
+ const classBody = classNode.body;
37364
+ if (classBody) walkAst(classBody, (node) => {
37365
+ if (node !== classBody && CLASS_NODE_TYPES.has(node.type)) return false;
37366
+ if (!isNodeOfType(node, "JSXAttribute") || !isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "ref" || !node.value || !isNodeOfType(node.value, "JSXExpressionContainer") || !node.value.expression) return;
37367
+ collectCallbackRefFieldsFromExpression(node.value.expression, classNode, fieldNames, scopes);
37368
+ });
37369
+ callbackRefFieldNamesByClass.set(classNode, fieldNames);
37370
+ return fieldNames;
37371
+ };
37372
+ const collectLifecycleWrittenFieldNames = (lifecycleFunction) => {
37373
+ const fieldNames = /* @__PURE__ */ new Set();
37374
+ const body = lifecycleFunction.body;
37375
+ if (!body) return fieldNames;
37376
+ walkAst(body, (node) => {
37377
+ if (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node)) return false;
37378
+ const target = isNodeOfType(node, "AssignmentExpression") && node.left || isNodeOfType(node, "UpdateExpression") && node.argument || null;
37379
+ if (!target) return;
37380
+ const fieldName = getThisFieldName(target);
37381
+ if (fieldName) fieldNames.add(fieldName);
37382
+ });
37383
+ return fieldNames;
37384
+ };
36532
37385
  const getThisStateFieldName = (node) => {
36533
37386
  const unwrappedNode = stripParenExpression(node);
36534
37387
  if (!isNodeOfType(unwrappedNode, "MemberExpression")) return null;
@@ -36546,15 +37399,17 @@ const collectLocalInitializers = (lifecycleFunction) => {
36546
37399
  });
36547
37400
  return initializers;
36548
37401
  };
36549
- const derivesFromPostMountValue = (node, localInitializers, visitedNames = /* @__PURE__ */ new Set()) => {
37402
+ const derivesFromPostMountValue = (node, localInitializers, callbackRefFieldNames, visitedNames = /* @__PURE__ */ new Set()) => {
36550
37403
  if (readsPostMountValue(node)) return true;
37404
+ const fieldName = getThisFieldName(node);
37405
+ if (fieldName && callbackRefFieldNames.has(fieldName)) return true;
36551
37406
  const referencedNames = /* @__PURE__ */ new Set();
36552
37407
  collectReferenceIdentifierNames(node, referencedNames);
36553
37408
  for (const referencedName of referencedNames) {
36554
37409
  if (visitedNames.has(referencedName)) continue;
36555
37410
  const initializer = localInitializers.get(referencedName);
36556
37411
  if (!initializer) continue;
36557
- if (derivesFromPostMountValue(initializer, localInitializers, new Set([...visitedNames, referencedName]))) return true;
37412
+ if (derivesFromPostMountValue(initializer, localInitializers, callbackRefFieldNames, new Set([...visitedNames, referencedName]))) return true;
36558
37413
  }
36559
37414
  return false;
36560
37415
  };
@@ -36568,50 +37423,84 @@ const getSetStateFieldValue = (setStateCall, fieldName) => {
36568
37423
  }
36569
37424
  return null;
36570
37425
  };
36571
- const isConvergentPostMountGuard = (test, setStateCall, localInitializers) => {
36572
- let qualifies = false;
36573
- walkAst(test, (node) => {
36574
- if (qualifies) return false;
36575
- if (!isNodeOfType(node, "BinaryExpression") || !EQUALITY_OPERATORS.has(node.operator)) return;
36576
- const leftFieldName = getThisStateFieldName(node.left);
36577
- const rightFieldName = getThisStateFieldName(node.right);
36578
- const fieldName = leftFieldName ?? rightFieldName;
36579
- const comparedValue = leftFieldName ? node.right : node.left;
36580
- if (!fieldName || !leftFieldName && !rightFieldName) return;
36581
- const assignedValue = getSetStateFieldValue(setStateCall, fieldName);
36582
- if (!assignedValue || !areExpressionsStructurallyEqual(comparedValue, assignedValue)) return;
36583
- if (!derivesFromPostMountValue(comparedValue, localInitializers)) return;
36584
- qualifies = true;
36585
- return false;
36586
- });
36587
- return qualifies;
36588
- };
36589
- const isDiffGuardTest = (test, paramNames, derivedNames) => {
36590
- if (referencesAnyName(test, paramNames)) return true;
36591
- let qualifies = false;
36592
- walkAst(test, (node) => {
36593
- if (qualifies) return false;
36594
- if (!isNodeOfType(node, "BinaryExpression")) return;
36595
- if (!EQUALITY_OPERATORS.has(node.operator)) return;
36596
- if (isStatefulOperand(node.left, paramNames, derivedNames) && isStatefulOperand(node.right, paramNames, derivedNames) && (referencesAnyName(node.left, derivedNames) || referencesAnyName(node.right, derivedNames))) {
36597
- qualifies = true;
36598
- return false;
36599
- }
36600
- });
36601
- return qualifies;
37426
+ const isConvergentPostMountGuard = (test, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch) => {
37427
+ const expression = stripParenExpression(test);
37428
+ if (isNodeOfType(expression, "LogicalExpression")) {
37429
+ if (expression.operator !== "&&" && expression.operator !== "||") return false;
37430
+ const leftIsConvergent = isConvergentPostMountGuard(expression.left, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch);
37431
+ const rightIsConvergent = isConvergentPostMountGuard(expression.right, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch);
37432
+ return isTruthfulBranch && expression.operator === "||" || !isTruthfulBranch && expression.operator === "&&" ? leftIsConvergent && rightIsConvergent : leftIsConvergent || rightIsConvergent;
37433
+ }
37434
+ if (!isNodeOfType(expression, "BinaryExpression") || !(isTruthfulBranch ? DIFFERENCE_OPERATORS.has(expression.operator) : EQUALITY_OPERATORS.has(expression.operator) && !DIFFERENCE_OPERATORS.has(expression.operator))) return false;
37435
+ const leftFieldName = getThisStateFieldName(expression.left);
37436
+ const rightFieldName = getThisStateFieldName(expression.right);
37437
+ const fieldName = leftFieldName ?? rightFieldName;
37438
+ const comparedValue = leftFieldName ? expression.right : expression.left;
37439
+ if (!fieldName) return false;
37440
+ const assignedValue = getSetStateFieldValue(setStateCall, fieldName);
37441
+ if (!assignedValue || !areExpressionsStructurallyEqual(comparedValue, assignedValue)) return false;
37442
+ return isUndefinedIdentifier(comparedValue) || derivesFromPostMountValue(comparedValue, localInitializers, callbackRefFieldNames);
37443
+ };
37444
+ const containsPositiveStateFieldTest = (test, fieldName) => {
37445
+ const unwrappedTest = stripParenExpression(test);
37446
+ if (getThisStateFieldName(unwrappedTest) === fieldName) return true;
37447
+ return isNodeOfType(unwrappedTest, "LogicalExpression") && unwrappedTest.operator === "&&" && (containsPositiveStateFieldTest(unwrappedTest.left, fieldName) || containsPositiveStateFieldTest(unwrappedTest.right, fieldName));
36602
37448
  };
36603
- const isInsideDiffGuard = (setStateCall) => {
37449
+ const isConvergentUndefinedClearGuard = (test, setStateCall) => {
37450
+ if (!isNodeOfType(setStateCall, "CallExpression")) return false;
37451
+ const argument = setStateCall.arguments?.[0];
37452
+ if (!argument || !isNodeOfType(argument, "ObjectExpression")) return false;
37453
+ for (const property of argument.properties ?? []) {
37454
+ if (!isNodeOfType(property, "Property") || property.computed === true || !isUndefinedIdentifier(property.value)) continue;
37455
+ const fieldName = isNodeOfType(property.key, "Identifier") && property.key.name || isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" && property.key.value || null;
37456
+ if (fieldName && containsPositiveStateFieldTest(test, fieldName)) return true;
37457
+ }
37458
+ return false;
37459
+ };
37460
+ const isDiffGuardTest = (test, paramNames, derivedNames, isTruthfulBranch) => {
37461
+ const expression = stripParenExpression(test);
37462
+ if (isNodeOfType(expression, "LogicalExpression")) {
37463
+ if (expression.operator !== "&&" && expression.operator !== "||") return false;
37464
+ const leftIsDiffGuard = isDiffGuardTest(expression.left, paramNames, derivedNames, isTruthfulBranch);
37465
+ const rightIsDiffGuard = isDiffGuardTest(expression.right, paramNames, derivedNames, isTruthfulBranch);
37466
+ return isTruthfulBranch && expression.operator === "||" || !isTruthfulBranch && expression.operator === "&&" ? leftIsDiffGuard && rightIsDiffGuard : leftIsDiffGuard || rightIsDiffGuard;
37467
+ }
37468
+ if (!isNodeOfType(expression, "BinaryExpression") || !(isTruthfulBranch ? DIFFERENCE_OPERATORS.has(expression.operator) : EQUALITY_OPERATORS.has(expression.operator) && !DIFFERENCE_OPERATORS.has(expression.operator))) return false;
37469
+ return isStatefulOperand(expression.left, paramNames, derivedNames) && isStatefulOperand(expression.right, paramNames, derivedNames) && (referencesAnyName(expression.left, paramNames) || referencesAnyName(expression.right, paramNames) || referencesAnyName(expression.left, derivedNames) || referencesAnyName(expression.right, derivedNames));
37470
+ };
37471
+ const isInsideDiffGuard = (setStateCall, scopes) => {
36604
37472
  const lifecycleFunction = findEnclosingLifecycleFunction(setStateCall);
36605
37473
  if (!lifecycleFunction) return false;
36606
37474
  const paramNames = /* @__PURE__ */ new Set();
36607
- for (const param of lifecycleFunction.params ?? []) collectPatternNames(param, paramNames);
37475
+ const parameters = lifecycleFunction.params ?? [];
37476
+ for (const param of parameters) collectPatternNames(param, paramNames);
37477
+ const previousSourcePaths = /* @__PURE__ */ new Map();
37478
+ const [previousPropsParameter, previousStateParameter] = parameters;
37479
+ collectPreviousSourcePaths(previousPropsParameter, "props", [], previousSourcePaths);
37480
+ collectPreviousSourcePaths(previousStateParameter, "state", [], previousSourcePaths);
36608
37481
  const derivedNames = collectDiffSourceLocalNames(lifecycleFunction, paramNames);
36609
37482
  const localInitializers = collectLocalInitializers(lifecycleFunction);
37483
+ const lifecycleWrittenFieldNames = collectLifecycleWrittenFieldNames(lifecycleFunction);
37484
+ const callbackRefFieldNames = new Set([...getCallbackRefFieldNames(findEnclosingClass(lifecycleFunction), scopes)].filter((fieldName) => !lifecycleWrittenFieldNames.has(fieldName)));
36610
37485
  let child = setStateCall;
36611
37486
  let ancestor = setStateCall.parent;
36612
37487
  while (ancestor && ancestor !== lifecycleFunction) {
36613
- 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;
36614
- if (guardTest && (isDiffGuardTest(guardTest, paramNames, derivedNames) || isConvergentPostMountGuard(guardTest, setStateCall, localInitializers))) return true;
37488
+ let guardTest = null;
37489
+ let isTruthfulBranch = true;
37490
+ if (isNodeOfType(ancestor, "IfStatement")) {
37491
+ if (child === ancestor.consequent) guardTest = ancestor.test;
37492
+ else if (child === ancestor.alternate) {
37493
+ guardTest = ancestor.test;
37494
+ isTruthfulBranch = false;
37495
+ }
37496
+ } else if (isNodeOfType(ancestor, "ConditionalExpression")) {
37497
+ if (child === ancestor.consequent) guardTest = ancestor.test;
37498
+ else if (child === ancestor.alternate) {
37499
+ guardTest = ancestor.test;
37500
+ isTruthfulBranch = false;
37501
+ }
37502
+ } else if (isNodeOfType(ancestor, "LogicalExpression") && ancestor.operator === "&&" && child === ancestor.right) guardTest = ancestor.left;
37503
+ if (guardTest && (isDiffGuardTest(guardTest, paramNames, derivedNames, isTruthfulBranch) || isTruthfulBranch && isHistoricalToCurrentTransitionGuard(guardTest, previousSourcePaths) || isConvergentPostMountGuard(guardTest, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch) || isTruthfulBranch && isConvergentUndefinedClearGuard(guardTest, setStateCall))) return true;
36615
37504
  child = ancestor;
36616
37505
  ancestor = ancestor.parent ?? null;
36617
37506
  }
@@ -36633,7 +37522,7 @@ const noDidUpdateSetState = defineRule({
36633
37522
  if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
36634
37523
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
36635
37524
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
36636
- if (isInsideDiffGuard(node)) return;
37525
+ if (isInsideDiffGuard(node, context.scopes)) return;
36637
37526
  context.report({
36638
37527
  node: node.callee,
36639
37528
  message: MESSAGE$27
@@ -40355,41 +41244,223 @@ const readLogicalConditionResult = (operator, leftResult, rightResult) => {
40355
41244
  if (leftResult === false && rightResult === false) return false;
40356
41245
  return null;
40357
41246
  };
40358
- const readHydrationConditionResult = (expression, context, runtime) => {
41247
+ const readHydrationConditionResult = (expression, context, runtime, state) => {
40359
41248
  const unwrappedExpression = stripParenExpression(expression);
40360
41249
  const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
40361
41250
  if (predicateMatch) return predicateMatch[`${runtime}Result`];
40362
41251
  const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
40363
41252
  if (staticResult !== null) return staticResult;
41253
+ const expressionSymbol = isNodeOfType(unwrappedExpression, "Identifier") ? context.scopes.symbolFor(unwrappedExpression) : null;
41254
+ const parameterValue = expressionSymbol ? state.parameterValuesBySymbolId.get(expressionSymbol.id) : null;
41255
+ if (expressionSymbol && parameterValue && !state.visitedSymbolIds.has(expressionSymbol.id)) {
41256
+ state.visitedSymbolIds.add(expressionSymbol.id);
41257
+ const result = readHydrationConditionResult(parameterValue, context, runtime, state);
41258
+ state.visitedSymbolIds.delete(expressionSymbol.id);
41259
+ return result;
41260
+ }
41261
+ if (expressionSymbol && expressionSymbol.kind === "const" && expressionSymbol.initializer && expressionSymbol.references.every((reference) => reference.flag === "read") && !state.visitedSymbolIds.has(expressionSymbol.id)) {
41262
+ state.visitedSymbolIds.add(expressionSymbol.id);
41263
+ const result = readHydrationConditionResult(expressionSymbol.initializer, context, runtime, state);
41264
+ state.visitedSymbolIds.delete(expressionSymbol.id);
41265
+ return result;
41266
+ }
41267
+ if (isNodeOfType(unwrappedExpression, "CallExpression")) {
41268
+ const callArguments = unwrappedExpression.arguments ?? [];
41269
+ if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
41270
+ allowGlobalReactNamespace: true,
41271
+ resolveNamedAliases: true
41272
+ })) {
41273
+ const callbackArgument = callArguments[0];
41274
+ if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
41275
+ const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
41276
+ return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? readHydrationFunctionResult(callbackFunction, context, runtime, state) : null;
41277
+ }
41278
+ const callee = stripParenExpression(unwrappedExpression.callee);
41279
+ if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return readHydrationConditionResult(callArguments[0], context, runtime, state);
41280
+ const helperFunction = resolveExactLocalFunction(callee, context.scopes);
41281
+ if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
41282
+ const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
41283
+ for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
41284
+ const parameter = helperFunction.params[parameterIndex];
41285
+ const argument = callArguments[parameterIndex];
41286
+ if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
41287
+ const parameterSymbol = context.scopes.symbolFor(parameter);
41288
+ if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
41289
+ }
41290
+ return readHydrationFunctionResult(helperFunction, context, runtime, {
41291
+ ...state,
41292
+ parameterValuesBySymbolId
41293
+ });
41294
+ }
40364
41295
  if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
40365
- const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
41296
+ const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime, state);
40366
41297
  return argumentResult === null ? null : !argumentResult;
40367
41298
  }
40368
41299
  if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
40369
- return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
41300
+ return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime, state), readHydrationConditionResult(unwrappedExpression.right, context, runtime, state));
41301
+ };
41302
+ const readHydrationStatementResult = (statement, context, runtime, state) => {
41303
+ if (isNodeOfType(statement, "ReturnStatement")) return {
41304
+ didReturn: true,
41305
+ value: statement.argument ? readHydrationConditionResult(statement.argument, context, runtime, state) : null
41306
+ };
41307
+ if (isNodeOfType(statement, "BlockStatement")) {
41308
+ for (const childStatement of statement.body) {
41309
+ const result = readHydrationStatementResult(childStatement, context, runtime, state);
41310
+ if (result.didReturn) return result;
41311
+ if (statementAlwaysExits(childStatement)) break;
41312
+ }
41313
+ return {
41314
+ didReturn: false,
41315
+ value: null
41316
+ };
41317
+ }
41318
+ if (!isNodeOfType(statement, "IfStatement")) return {
41319
+ didReturn: false,
41320
+ value: null
41321
+ };
41322
+ const conditionResult = readHydrationConditionResult(statement.test, context, runtime, state);
41323
+ if (conditionResult !== null) {
41324
+ const selectedBranch = conditionResult ? statement.consequent : statement.alternate;
41325
+ return selectedBranch ? readHydrationStatementResult(selectedBranch, context, runtime, state) : {
41326
+ didReturn: false,
41327
+ value: null
41328
+ };
41329
+ }
41330
+ const consequentResult = readHydrationStatementResult(statement.consequent, context, runtime, state);
41331
+ const alternateResult = statement.alternate ? readHydrationStatementResult(statement.alternate, context, runtime, state) : {
41332
+ didReturn: false,
41333
+ value: null
41334
+ };
41335
+ return consequentResult.didReturn && alternateResult.didReturn && consequentResult.value !== null && consequentResult.value === alternateResult.value ? consequentResult : {
41336
+ didReturn: consequentResult.didReturn || alternateResult.didReturn,
41337
+ value: null
41338
+ };
40370
41339
  };
40371
- const matchHydrationCondition = (expression, context) => {
41340
+ const readHydrationFunctionResult = (functionNode, context, runtime, state) => {
41341
+ if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
41342
+ state.visitedFunctionNodes.add(functionNode);
41343
+ const result = isNodeOfType(functionNode.body, "BlockStatement") ? readHydrationStatementResult(functionNode.body, context, runtime, state).value : readHydrationConditionResult(functionNode.body, context, runtime, state);
41344
+ state.visitedFunctionNodes.delete(functionNode);
41345
+ return result;
41346
+ };
41347
+ const doEquivalentExpressionBindingsMatch = (leftExpression, rightExpression, scopes) => {
41348
+ const left = stripParenExpression(leftExpression);
41349
+ const right = stripParenExpression(rightExpression);
41350
+ if (isNodeOfType(left, "Identifier") && isNodeOfType(right, "Identifier")) {
41351
+ const leftSymbol = scopes.symbolFor(left);
41352
+ const rightSymbol = scopes.symbolFor(right);
41353
+ return leftSymbol || rightSymbol ? leftSymbol?.id === rightSymbol?.id : true;
41354
+ }
41355
+ if (isNodeOfType(left, "MemberExpression") && isNodeOfType(right, "MemberExpression")) return doEquivalentExpressionBindingsMatch(left.object, right.object, scopes) && (!left.computed || doEquivalentExpressionBindingsMatch(left.property, right.property, scopes));
41356
+ if (isNodeOfType(left, "CallExpression") && isNodeOfType(right, "CallExpression")) {
41357
+ const rightArguments = right.arguments ?? [];
41358
+ return doEquivalentExpressionBindingsMatch(left.callee, right.callee, scopes) && (left.arguments ?? []).every((argument, index) => {
41359
+ const rightArgument = rightArguments[index];
41360
+ return Boolean(rightArgument && doEquivalentExpressionBindingsMatch(argument, rightArgument, scopes));
41361
+ });
41362
+ }
41363
+ return true;
41364
+ };
41365
+ const areHelperReturnValuesEquivalent = (leftValue, rightValue, context) => {
41366
+ if (areExpressionsStructurallyEqual(leftValue, rightValue)) return doEquivalentExpressionBindingsMatch(leftValue, rightValue, context.scopes);
41367
+ const leftBoolean = readInitialStateBoolean(leftValue, context.scopes);
41368
+ const rightBoolean = readInitialStateBoolean(rightValue, context.scopes);
41369
+ return leftBoolean !== null && rightBoolean !== null && leftBoolean === rightBoolean;
41370
+ };
41371
+ const doHelperReturnValuesDiffer = (leftValues, rightValues, context) => {
41372
+ const everyValueHasEquivalent = (values, candidateValues) => values.every((value) => candidateValues.some((candidateValue) => areHelperReturnValuesEquivalent(value, candidateValue, context)));
41373
+ return !everyValueHasEquivalent(leftValues, rightValues) || !everyValueHasEquivalent(rightValues, leftValues);
41374
+ };
41375
+ const matchHydrationConditionInternal = (expression, context, state) => {
40372
41376
  const unwrappedExpression = stripParenExpression(expression);
40373
41377
  const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
40374
41378
  if (predicateMatch) return {
40375
41379
  predicateMatch,
40376
41380
  predicateNode: unwrappedExpression
40377
41381
  };
40378
- if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationCondition(unwrappedExpression.argument, context);
40379
- if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
40380
- const leftMatch = matchHydrationCondition(unwrappedExpression.left, context);
40381
- const rightMatch = matchHydrationCondition(unwrappedExpression.right, context);
40382
- if (leftMatch && rightMatch) {
40383
- const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client");
40384
- const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server");
40385
- return clientResult !== null && serverResult !== null && clientResult !== serverResult ? leftMatch : null;
41382
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
41383
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
41384
+ const parameterValue = symbol ? state.parameterValuesBySymbolId.get(symbol.id) : null;
41385
+ if (symbol && parameterValue && !state.visitedSymbolIds.has(symbol.id)) {
41386
+ state.visitedSymbolIds.add(symbol.id);
41387
+ const match = matchHydrationConditionInternal(parameterValue, context, state);
41388
+ state.visitedSymbolIds.delete(symbol.id);
41389
+ return match;
41390
+ }
41391
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer || symbol.references.some((reference) => reference.flag !== "read") || state.visitedSymbolIds.has(symbol.id)) return null;
41392
+ state.visitedSymbolIds.add(symbol.id);
41393
+ const match = matchHydrationConditionInternal(symbol.initializer, context, state);
41394
+ state.visitedSymbolIds.delete(symbol.id);
41395
+ return match;
41396
+ }
41397
+ if (isNodeOfType(unwrappedExpression, "CallExpression")) {
41398
+ const callArguments = unwrappedExpression.arguments ?? [];
41399
+ if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
41400
+ allowGlobalReactNamespace: true,
41401
+ resolveNamedAliases: true
41402
+ })) {
41403
+ const callbackArgument = callArguments[0];
41404
+ if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
41405
+ const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
41406
+ return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? matchHydrationFunctionResult(callbackFunction, context, state) : null;
41407
+ }
41408
+ const callee = stripParenExpression(unwrappedExpression.callee);
41409
+ if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return matchHydrationConditionInternal(callArguments[0], context, state);
41410
+ const helperFunction = resolveExactLocalFunction(callee, context.scopes);
41411
+ if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
41412
+ const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
41413
+ for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
41414
+ const parameter = helperFunction.params[parameterIndex];
41415
+ const argument = callArguments[parameterIndex];
41416
+ if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
41417
+ const parameterSymbol = context.scopes.symbolFor(parameter);
41418
+ if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
41419
+ }
41420
+ return matchHydrationFunctionResult(helperFunction, context, {
41421
+ ...state,
41422
+ parameterValuesBySymbolId
41423
+ });
40386
41424
  }
41425
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationConditionInternal(unwrappedExpression.argument, context, state);
41426
+ if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
41427
+ const leftMatch = matchHydrationConditionInternal(unwrappedExpression.left, context, state);
41428
+ const rightMatch = matchHydrationConditionInternal(unwrappedExpression.right, context, state);
40387
41429
  const nestedMatch = leftMatch ?? rightMatch;
40388
41430
  if (!nestedMatch) return null;
40389
- const otherResult = readInitialStateBoolean(leftMatch ? unwrappedExpression.right : unwrappedExpression.left, context.scopes);
40390
- if (unwrappedExpression.operator === "&&" && otherResult === false || unwrappedExpression.operator === "||" && otherResult === true) return null;
40391
- return nestedMatch;
41431
+ const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client", state);
41432
+ const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server", state);
41433
+ return clientResult !== null && serverResult !== null && clientResult === serverResult ? null : nestedMatch;
40392
41434
  };
41435
+ const matchHydrationReturningStatement = (statement, context, state) => {
41436
+ if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? matchHydrationConditionInternal(statement.argument, context, state) : null;
41437
+ if (isNodeOfType(statement, "IfStatement")) {
41438
+ const conditionMatch = matchHydrationConditionInternal(statement.test, context, state);
41439
+ const consequentValues = getReturnedValues(statement.consequent);
41440
+ const alternateValues = statement.alternate ? getReturnedValues(statement.alternate) : findFollowingReturnedValues(statement);
41441
+ if (conditionMatch && consequentValues.length > 0 && alternateValues.length > 0 && doHelperReturnValuesDiffer(consequentValues, alternateValues, context)) return conditionMatch;
41442
+ return matchHydrationReturningStatement(statement.consequent, context, state) ?? (statement.alternate ? matchHydrationReturningStatement(statement.alternate, context, state) : null);
41443
+ }
41444
+ if (!isNodeOfType(statement, "BlockStatement")) return null;
41445
+ for (const childStatement of statement.body) {
41446
+ const match = matchHydrationReturningStatement(childStatement, context, state);
41447
+ if (match) return match;
41448
+ if (statementAlwaysExits(childStatement)) break;
41449
+ }
41450
+ return null;
41451
+ };
41452
+ const matchHydrationFunctionResult = (functionNode, context, state) => {
41453
+ if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
41454
+ state.visitedFunctionNodes.add(functionNode);
41455
+ const match = isNodeOfType(functionNode.body, "BlockStatement") ? matchHydrationReturningStatement(functionNode.body, context, state) : matchHydrationConditionInternal(functionNode.body, context, state);
41456
+ state.visitedFunctionNodes.delete(functionNode);
41457
+ return match;
41458
+ };
41459
+ const matchHydrationCondition = (expression, context) => matchHydrationConditionInternal(expression, context, {
41460
+ parameterValuesBySymbolId: /* @__PURE__ */ new Map(),
41461
+ visitedFunctionNodes: /* @__PURE__ */ new Set(),
41462
+ visitedSymbolIds: /* @__PURE__ */ new Set()
41463
+ });
40393
41464
  const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
40394
41465
  const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
40395
41466
  if (!leftNode || !rightNode) return leftNode === rightNode;
@@ -40532,17 +41603,17 @@ const noHydrationBranchOnBrowserGlobal = defineRule({
40532
41603
  const { predicateMatch, predicateNode } = conditionMatch;
40533
41604
  if (reportedNodes.has(predicateNode)) return;
40534
41605
  if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
40535
- const componentOrHookNode = findRenderPhaseComponentOrHook(predicateNode, context.scopes);
41606
+ const componentOrHookNode = findRenderPhaseComponentOrHook(conditionNode, context.scopes);
40536
41607
  if (!componentOrHookNode) return;
40537
41608
  if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
40538
- if (requiresRenderedContext && !isInRenderedOutput(predicateNode, componentOrHookNode, context.scopes)) return;
41609
+ if (requiresRenderedContext && !isInRenderedOutput(conditionNode, componentOrHookNode, context.scopes)) return;
40539
41610
  if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
40540
- const attribute = findEnclosingJsxAttribute(predicateNode);
41611
+ const attribute = findEnclosingJsxAttribute(conditionNode);
40541
41612
  if (!attribute || isEventHandlerAttribute(attribute)) return;
40542
41613
  }
40543
- if (fileIsEmailTemplate || isGatedByFalsyInitialState(predicateNode, context.scopes)) return;
40544
- if (isAfterClientOnlyEarlyReturn(predicateNode, componentOrHookNode, context.scopes)) return;
40545
- const openingElement = findEnclosingJsxOpeningElement(predicateNode);
41614
+ if (fileIsEmailTemplate || isGatedByFalsyInitialState(conditionNode, context.scopes)) return;
41615
+ if (isAfterClientOnlyEarlyReturn(conditionNode, componentOrHookNode, context.scopes)) return;
41616
+ const openingElement = findEnclosingJsxOpeningElement(conditionNode);
40546
41617
  if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
40547
41618
  if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
40548
41619
  if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
@@ -40924,7 +41995,7 @@ const noInitializeState = defineRule({
40924
41995
  if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
40925
41996
  const analysis = getProgramAnalysis(node);
40926
41997
  if (!analysis) return;
40927
- for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
41998
+ for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
40928
41999
  if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
40929
42000
  const stateName = getStateName(fact.stateDeclarator);
40930
42001
  context.report({
@@ -41282,7 +42353,8 @@ const noJsxElementType = defineRule({
41282
42353
  create: (context) => {
41283
42354
  let isJsxImported = false;
41284
42355
  const flaggedAnnotations = [];
41285
- const checkReturnType = (returnType) => {
42356
+ const collectComponentReturnType = (functionNode, returnType) => {
42357
+ if (!(isNodeOfType(functionNode, "TSDeclareFunction") ? Boolean(functionNode.id && isReactComponentName(functionNode.id.name)) : isComponentFunction$1(functionNode))) return;
41286
42358
  const typeAnnotation = extractReturnTypeAnnotation(returnType);
41287
42359
  if (!typeAnnotation) return;
41288
42360
  if (isJsxElementTypeReference(typeAnnotation)) flaggedAnnotations.push(typeAnnotation);
@@ -41292,19 +42364,16 @@ const noJsxElementType = defineRule({
41292
42364
  if (isJsxImportBinding(node)) isJsxImported = true;
41293
42365
  },
41294
42366
  FunctionDeclaration(node) {
41295
- checkReturnType(node.returnType);
42367
+ collectComponentReturnType(node, node.returnType);
41296
42368
  },
41297
42369
  ArrowFunctionExpression(node) {
41298
- checkReturnType(node.returnType);
42370
+ collectComponentReturnType(node, node.returnType);
41299
42371
  },
41300
42372
  FunctionExpression(node) {
41301
- checkReturnType(node.returnType);
42373
+ collectComponentReturnType(node, node.returnType);
41302
42374
  },
41303
42375
  TSDeclareFunction(node) {
41304
- checkReturnType(node.returnType);
41305
- },
41306
- TSMethodSignature(node) {
41307
- checkReturnType(node.returnType);
42376
+ collectComponentReturnType(node, node.returnType);
41308
42377
  },
41309
42378
  "Program:exit"() {
41310
42379
  if (isJsxImported) return;
@@ -44331,6 +45400,114 @@ const DATA_SINK_METHOD_NAMES = new Set([
44331
45400
  "deserialize"
44332
45401
  ]);
44333
45402
  //#endregion
45403
+ //#region src/plugin/utils/get-transparent-react-callback-wrapper-argument.ts
45404
+ const getTransparentReactCallbackWrapperArgument = (initializer, resultSymbol, scopes) => {
45405
+ const callExpression = stripParenExpression(initializer);
45406
+ if (!isNodeOfType(callExpression, "CallExpression")) return null;
45407
+ const callbackArgument = callExpression.arguments[0];
45408
+ if (!callbackArgument) return null;
45409
+ if (resultSymbol && symbolHasReactUseEffectEventOrigin(resultSymbol, scopes)) return callbackArgument;
45410
+ return isReactApiCall(callExpression, "useCallback", scopes, {
45411
+ allowGlobalReactNamespace: true,
45412
+ allowUnboundBareCalls: true
45413
+ }) ? callbackArgument : null;
45414
+ };
45415
+ //#endregion
45416
+ //#region src/plugin/rules/state-and-effects/utils/resolve-parent-callback-provenance.ts
45417
+ const getDeclarationKind$1 = (declarator) => {
45418
+ const declaration = declarator.parent;
45419
+ return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
45420
+ };
45421
+ const hasMutableBindingWrite$2 = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
45422
+ const mergeRequiredBranches = (leftNames, rightNames) => {
45423
+ if (!leftNames || !rightNames) return null;
45424
+ return new Set([...leftNames, ...rightNames]);
45425
+ };
45426
+ const getPropReferenceName = (analysis, identifier) => {
45427
+ if (!isNodeOfType(identifier, "Identifier")) return null;
45428
+ const reference = getRef(analysis, identifier);
45429
+ if (!reference || !isProp(analysis, reference) || isWholePropsObjectReference(analysis, reference)) return null;
45430
+ const bindingIdentifier = (reference.resolved?.defs.find((definition) => definition.type === "Parameter"))?.name;
45431
+ return (bindingIdentifier && getDestructuredBindingPropertyName(bindingIdentifier)) ?? identifier.name;
45432
+ };
45433
+ const getSingleConstDeclarator = (reference) => {
45434
+ if (!reference.resolved || hasMutableBindingWrite$2(reference)) return null;
45435
+ const declarators = reference.resolved.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
45436
+ if (declarators.length !== 1) return null;
45437
+ const declarator = declarators[0];
45438
+ if (!declarator || getDeclarationKind$1(declarator) !== "const") return null;
45439
+ return declarator;
45440
+ };
45441
+ const resolveParentCallbackPropNames = (analysis, expression, scopes, visitedReferences, allowFunctionForwarder = false) => {
45442
+ const unwrappedExpression = stripParenExpression(expression);
45443
+ if (isFunctionLike$1(unwrappedExpression)) {
45444
+ if (!allowFunctionForwarder || Boolean(unwrappedExpression.async)) return null;
45445
+ const callbackNames = /* @__PURE__ */ new Set();
45446
+ walkInsideStatementBlocks(unwrappedExpression.body, (child) => {
45447
+ if (!isNodeOfType(child, "CallExpression")) return;
45448
+ const resolvedNames = resolveParentCallbackPropNames(analysis, child.callee, scopes, new Set(visitedReferences), false);
45449
+ if (!resolvedNames) return;
45450
+ for (const resolvedName of resolvedNames) callbackNames.add(resolvedName);
45451
+ });
45452
+ return callbackNames.size > 0 ? callbackNames : null;
45453
+ }
45454
+ if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return mergeRequiredBranches(resolveParentCallbackPropNames(analysis, unwrappedExpression.consequent, scopes, new Set(visitedReferences), false), resolveParentCallbackPropNames(analysis, unwrappedExpression.alternate, scopes, new Set(visitedReferences), false));
45455
+ if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return mergeRequiredBranches(resolveParentCallbackPropNames(analysis, unwrappedExpression.left, scopes, new Set(visitedReferences), false), resolveParentCallbackPropNames(analysis, unwrappedExpression.right, scopes, new Set(visitedReferences)));
45456
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
45457
+ const propName = getPropReferenceName(analysis, unwrappedExpression);
45458
+ if (propName) return new Set([propName]);
45459
+ const reference = getRef(analysis, unwrappedExpression);
45460
+ if (!reference?.resolved || visitedReferences.has(reference.resolved)) return null;
45461
+ const declarator = getSingleConstDeclarator(reference);
45462
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
45463
+ visitedReferences.add(reference.resolved);
45464
+ const wrappedArgument = getTransparentReactCallbackWrapperArgument(declarator.init, scopes.symbolFor(unwrappedExpression), scopes);
45465
+ const allowsFunctionForwarder = Boolean(wrappedArgument && !isReactApiCall(declarator.init, "useCallback", scopes, {
45466
+ allowGlobalReactNamespace: true,
45467
+ allowUnboundBareCalls: true
45468
+ }));
45469
+ return resolveParentCallbackPropNames(analysis, wrappedArgument ?? declarator.init, scopes, visitedReferences, allowsFunctionForwarder);
45470
+ }
45471
+ if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
45472
+ const propertyName = getStaticMemberPropertyName(unwrappedExpression);
45473
+ if (!propertyName) return null;
45474
+ const receiver = stripParenExpression(unwrappedExpression.object);
45475
+ if (!isNodeOfType(receiver, "Identifier")) return null;
45476
+ const receiverReference = getRef(analysis, receiver);
45477
+ if (!receiverReference?.resolved || visitedReferences.has(receiverReference.resolved)) return null;
45478
+ if (isWholePropsObjectReference(analysis, receiverReference)) return new Set([propertyName]);
45479
+ const declarator = getSingleConstDeclarator(receiverReference);
45480
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
45481
+ visitedReferences.add(receiverReference.resolved);
45482
+ const initializer = stripParenExpression(declarator.init);
45483
+ if (propertyName === "current" && isNodeOfType(initializer, "CallExpression")) {
45484
+ if (!isReactApiCall(initializer, "useRef", scopes, {
45485
+ allowGlobalReactNamespace: true,
45486
+ allowUnboundBareCalls: true
45487
+ })) return null;
45488
+ const callbackArgument = initializer.arguments[0];
45489
+ if (!callbackArgument) return null;
45490
+ let callbackNames = resolveParentCallbackPropNames(analysis, callbackArgument, scopes, new Set(visitedReferences), false);
45491
+ if (!callbackNames) return null;
45492
+ for (const candidateReference of receiverReference.resolved.references) {
45493
+ const candidateIdentifier = candidateReference.identifier;
45494
+ const candidateMember = candidateIdentifier.parent;
45495
+ if (!candidateMember || !isNodeOfType(candidateMember, "MemberExpression") || candidateMember.object !== candidateIdentifier || getStaticMemberPropertyName(candidateMember) !== "current") continue;
45496
+ const assignment = candidateMember.parent;
45497
+ if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== candidateMember) continue;
45498
+ if (assignment.operator !== "=") return null;
45499
+ callbackNames = mergeRequiredBranches(callbackNames, resolveParentCallbackPropNames(analysis, assignment.right, scopes, new Set(visitedReferences), false));
45500
+ if (!callbackNames) return null;
45501
+ }
45502
+ return callbackNames;
45503
+ }
45504
+ if (!isNodeOfType(initializer, "ObjectExpression")) return null;
45505
+ const property = initializer.properties.find((candidateProperty) => isNodeOfType(candidateProperty, "Property") && getStaticPropertyKeyName(candidateProperty, { allowComputedString: true }) === propertyName);
45506
+ if (!property || !isNodeOfType(property, "Property")) return null;
45507
+ return resolveParentCallbackPropNames(analysis, property.value, scopes, visitedReferences, false);
45508
+ };
45509
+ const getParentCallbackPropNames = ({ analysis, expression, scopes }) => resolveParentCallbackPropNames(analysis, expression, scopes, /* @__PURE__ */ new Set(), false);
45510
+ //#endregion
44334
45511
  //#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
44335
45512
  const isUseStateIdentifier = (identifier) => {
44336
45513
  if (!isNodeOfType(identifier, "Identifier")) return false;
@@ -44359,14 +45536,18 @@ const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
44359
45536
  "useStableCallback",
44360
45537
  "useCallbackRef"
44361
45538
  ]);
44362
- const getWrapperHookWrappedFunction = (initializer) => {
45539
+ const getWrapperHookWrappedFunction = (initializer, resultSymbol, scopes) => {
44363
45540
  if (!isNodeOfType(initializer, "CallExpression")) return null;
45541
+ const transparentReactArgument = getTransparentReactCallbackWrapperArgument(initializer, resultSymbol, scopes);
45542
+ if (transparentReactArgument) return transparentReactArgument;
44364
45543
  const callee = initializer.callee;
44365
45544
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") ? callee.property.name : null;
44366
45545
  if (!calleeName || !FUNCTION_WRAPPER_HOOK_NAMES$1.has(calleeName)) return null;
44367
45546
  const wrapped = initializer.arguments?.[0];
44368
- if (!wrapped || !isFunctionLike$1(wrapped)) return null;
44369
- return wrapped;
45547
+ if (!wrapped) return null;
45548
+ if (calleeName === "useEffectEvent") return null;
45549
+ if (isFunctionLike$1(wrapped)) return wrapped;
45550
+ return null;
44370
45551
  };
44371
45552
  const HANDLER_NAMED_PROP_PATTERN = /^(on|handle)[A-Z]/;
44372
45553
  const wrappedFunctionNotifiesParent = (analysis, wrappedFunction) => getDownstreamRefs(analysis, wrappedFunction).some((innerRef) => {
@@ -44376,16 +45557,29 @@ const wrappedFunctionNotifiesParent = (analysis, wrappedFunction) => getDownstre
44376
45557
  const innerParent = innerIdentifier.parent;
44377
45558
  return Boolean(innerParent && isNodeOfType(innerParent, "CallExpression") && innerParent.callee === innerIdentifier);
44378
45559
  });
44379
- const isDirectParentCallbackRef = (analysis, ref) => {
45560
+ const isDirectParentCallbackRef = (analysis, ref, scopes) => {
44380
45561
  if (isProp(analysis, ref)) return true;
45562
+ if (hasMutableBindingWrite$1(ref)) {
45563
+ if (!(ref.resolved?.references.filter((candidateReference) => candidateReference.isWrite() && !candidateReference.init) ?? []).every((candidateReference) => {
45564
+ const candidateIdentifier = candidateReference.identifier;
45565
+ const assignment = candidateIdentifier.parent;
45566
+ if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== candidateIdentifier) return false;
45567
+ const assignedReferences = getDownstreamRefs(analysis, assignment.right);
45568
+ return assignedReferences.length > 0 && assignedReferences.every((assignedReference) => isProp(analysis, assignedReference));
45569
+ })) return false;
45570
+ }
44381
45571
  return Boolean(ref.resolved?.defs.some((def) => {
44382
45572
  const node = def.node;
44383
45573
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
44384
45574
  const initializer = unwrapChainExpression(node.init);
44385
- const wrappedFunction = getWrapperHookWrappedFunction(initializer);
45575
+ const wrappedFunction = getWrapperHookWrappedFunction(initializer, isNodeOfType(node.id, "Identifier") ? scopes.symbolFor(node.id) ?? null : null, scopes);
44386
45576
  if (wrappedFunction) {
44387
45577
  if (wrappedFunction.async) return false;
44388
- return wrappedFunctionNotifiesParent(analysis, wrappedFunction);
45578
+ if (isFunctionLike$1(wrappedFunction)) return wrappedFunctionNotifiesParent(analysis, wrappedFunction);
45579
+ const directName = getParentCallbackPropName(analysis, wrappedFunction);
45580
+ const downstreamReferences = getDownstreamRefs(analysis, wrappedFunction);
45581
+ if (directName !== null) return true;
45582
+ return downstreamReferences.some((wrappedReference) => !hasMutableBindingWrite$1(wrappedReference) && getUpstreamRefs(analysis, wrappedReference).some((upstreamReference) => isProp(analysis, upstreamReference)));
44389
45583
  }
44390
45584
  if (!isNodeOfType(initializer, "Identifier") && !isNodeOfType(initializer, "MemberExpression")) return false;
44391
45585
  return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
@@ -44395,7 +45589,7 @@ const getDeclarationKind = (declarator) => {
44395
45589
  const declaration = declarator.parent;
44396
45590
  return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
44397
45591
  };
44398
- const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
45592
+ const hasMutableBindingWrite$1 = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
44399
45593
  const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
44400
45594
  const unwrappedExpression = stripParenExpression(expression);
44401
45595
  if (isNodeOfType(unwrappedExpression, "Identifier")) {
@@ -44407,7 +45601,7 @@ const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @
44407
45601
  const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
44408
45602
  return (bindingIdentifier && getDestructuredBindingPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
44409
45603
  }
44410
- if (hasMutableBindingWrite(callbackReference)) return null;
45604
+ if (hasMutableBindingWrite$1(callbackReference)) return null;
44411
45605
  const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
44412
45606
  if (definitions.length !== 1) return null;
44413
45607
  const declarator = definitions[0];
@@ -44473,7 +45667,7 @@ const getRefAliasDeclarator = (identifier) => {
44473
45667
  const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
44474
45668
  if (!isNodeOfType(receiver, "Identifier")) return null;
44475
45669
  const receiverReference = getRef(analysis, receiver);
44476
- if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
45670
+ if (!receiverReference?.resolved || hasMutableBindingWrite$1(receiverReference)) return null;
44477
45671
  const variables = /* @__PURE__ */ new Set();
44478
45672
  let currentVariable = receiverReference.resolved;
44479
45673
  let refCall = null;
@@ -44489,7 +45683,7 @@ const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
44489
45683
  }
44490
45684
  if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
44491
45685
  const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
44492
- if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
45686
+ if (!upstreamReference?.resolved || hasMutableBindingWrite$1(upstreamReference)) return null;
44493
45687
  currentVariable = upstreamReference.resolved;
44494
45688
  }
44495
45689
  if (!refCall) return null;
@@ -44564,7 +45758,7 @@ const isParentPropsContextMerge = (analysis, expression) => {
44564
45758
  while (isNodeOfType(currentExpression, "Identifier")) {
44565
45759
  const currentReference = getRef(analysis, currentExpression);
44566
45760
  const currentVariable = currentReference?.resolved;
44567
- if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite(currentReference)) return false;
45761
+ if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite$1(currentReference)) return false;
44568
45762
  visitedVariables.add(currentVariable);
44569
45763
  const definitions = currentVariable.defs.filter((definition) => isNodeOfType(definition.node, "VariableDeclarator"));
44570
45764
  if (definitions.length !== 1) return false;
@@ -44578,11 +45772,11 @@ const isParentPropsContextMerge = (analysis, expression) => {
44578
45772
  const propsExpression = stripParenExpression(propsSpread.argument);
44579
45773
  if (!isNodeOfType(propsExpression, "Identifier")) return false;
44580
45774
  const propsReference = getRef(analysis, propsExpression);
44581
- if (!propsReference?.resolved || !isWholePropsObjectReference(analysis, propsReference) || hasMutableBindingWrite(propsReference) || propsReference.resolved.references.some((candidateReference) => candidateReference !== propsReference)) return false;
45775
+ if (!propsReference?.resolved || !isWholePropsObjectReference(analysis, propsReference) || hasMutableBindingWrite$1(propsReference) || propsReference.resolved.references.some((candidateReference) => candidateReference !== propsReference)) return false;
44582
45776
  const contextExpression = stripParenExpression(contextSpread.argument);
44583
45777
  if (!isNodeOfType(contextExpression, "Identifier")) return false;
44584
45778
  const contextReference = getRef(analysis, contextExpression);
44585
- if (!contextReference?.resolved || hasMutableBindingWrite(contextReference) || contextReference.resolved.references.some((candidateReference) => !candidateReference.init && candidateReference !== contextReference)) return false;
45779
+ if (!contextReference?.resolved || hasMutableBindingWrite$1(contextReference) || contextReference.resolved.references.some((candidateReference) => !candidateReference.init && candidateReference !== contextReference)) return false;
44586
45780
  const contextInitializer = contextReference.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
44587
45781
  if (!contextInitializer || !isNodeOfType(contextInitializer, "VariableDeclarator") || getDeclarationKind(contextInitializer) !== "const" || !contextInitializer.init || !isNodeOfType(contextInitializer.init, "CallExpression")) return false;
44588
45782
  const contextHook = stripParenExpression(contextInitializer.init.callee);
@@ -44596,7 +45790,7 @@ const getImmutableParentCallbackPropName = (analysis, expression) => {
44596
45790
  while (isNodeOfType(currentExpression, "Identifier")) {
44597
45791
  const currentReference = getRef(analysis, currentExpression);
44598
45792
  const currentVariable = currentReference?.resolved;
44599
- if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite(currentReference)) return null;
45793
+ if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite$1(currentReference)) return null;
44600
45794
  visitedVariables.add(currentVariable);
44601
45795
  const definition = currentVariable.defs.length === 1 ? currentVariable.defs[0] : null;
44602
45796
  const bindingIdentifier = definition?.name;
@@ -44665,7 +45859,7 @@ const getCommandCallbackPropName = (analysis, expression, isReactUseRefCall) =>
44665
45859
  while (isNodeOfType(currentExpression, "Identifier")) {
44666
45860
  const callbackReference = getRef(analysis, currentExpression);
44667
45861
  const callbackVariable = callbackReference?.resolved;
44668
- if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable) || hasMutableBindingWrite(callbackReference)) return null;
45862
+ if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable) || hasMutableBindingWrite$1(callbackReference)) return null;
44669
45863
  visitedVariables.add(callbackVariable);
44670
45864
  const definition = callbackVariable.defs.length === 1 ? callbackVariable.defs[0] : null;
44671
45865
  const declarator = definition?.node;
@@ -44685,10 +45879,11 @@ const getCommandCallbackPropName = (analysis, expression, isReactUseRefCall) =>
44685
45879
  if (!propertyName || !COMMAND_PROP_NAME_PATTERN.test(propertyName)) return null;
44686
45880
  return refCurrentObjectPreservesCallbackProperty(analysis, currentExpression.object, propertyName, isReactUseRefCall) ? propertyName : null;
44687
45881
  };
44688
- const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
45882
+ const isWrapperHookCallbackRef = (analysis, ref, scopes) => Boolean(ref.resolved?.defs.some((def) => {
44689
45883
  const node = def.node;
44690
45884
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
44691
- return getWrapperHookWrappedFunction(unwrapChainExpression(node.init)) !== null;
45885
+ const resultSymbol = isNodeOfType(node.id, "Identifier") ? scopes.symbolFor(node.id) ?? null : null;
45886
+ return getWrapperHookWrappedFunction(unwrapChainExpression(node.init), resultSymbol, scopes) !== null;
44692
45887
  }));
44693
45888
  const isHandlerBagArgument = (analysis, argument) => {
44694
45889
  if (!isNodeOfType(argument, "ObjectExpression")) return false;
@@ -44707,14 +45902,25 @@ const isHandlerBagArgument = (analysis, argument) => {
44707
45902
  };
44708
45903
  const getFunctionalUpdaterDataRefs = (analysis, updater) => getDownstreamRefs(analysis, updater).filter((updaterRef) => !updaterRef.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updater));
44709
45904
  const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
44710
- const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
45905
+ const EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1 = new Set([
44711
45906
  "useIntersectionObserver",
44712
45907
  "useMatchMedia",
45908
+ "useMediaJobProgress",
44713
45909
  "useMediaQuery",
44714
45910
  "useResizeObserver",
44715
45911
  "useVisibility",
44716
45912
  "useWindowSize"
44717
45913
  ]);
45914
+ const isCallbackPropReference = (analysis, ref) => {
45915
+ if (!isProp(analysis, ref)) return false;
45916
+ const identifier = ref.identifier;
45917
+ if (!isNodeOfType(identifier, "Identifier")) return false;
45918
+ if (!isWholePropsObjectReference(analysis, ref)) return HANDLER_NAMED_PROP_PATTERN.test(identifier.name);
45919
+ const member = identifier.parent;
45920
+ if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== identifier) return false;
45921
+ const propertyName = getStaticMemberPropertyName(member);
45922
+ return Boolean(propertyName && HANDLER_NAMED_PROP_PATTERN.test(propertyName));
45923
+ };
44718
45924
  const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
44719
45925
  const node = def.node;
44720
45926
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
@@ -44722,7 +45928,7 @@ const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs
44722
45928
  if (!isNodeOfType(init, "CallExpression")) return false;
44723
45929
  const callee = init.callee;
44724
45930
  if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
44725
- return (init.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isProp(analysis, downstreamRef)));
45931
+ return (init.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
44726
45932
  }));
44727
45933
  const isParentWiredHookResultArgument = (analysis, argument) => {
44728
45934
  if (!isNodeOfType(argument, "Identifier")) return false;
@@ -44735,19 +45941,19 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
44735
45941
  if (!isNodeOfType(identifier, "Identifier") || !HOOK_NAME_PATTERN$1.test(identifier.name)) return false;
44736
45942
  const parent = identifier.parent;
44737
45943
  if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
44738
- return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isProp(analysis, downstreamRef)));
45944
+ return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
44739
45945
  };
44740
45946
  const isExternalSubscriptionHookRef = (ref) => {
44741
45947
  const identifier = ref.identifier;
44742
45948
  if (!isNodeOfType(identifier, "Identifier")) return false;
44743
- if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(identifier.name) && isCalleePosition(identifier)) return true;
45949
+ if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(identifier.name) && isCalleePosition(identifier)) return true;
44744
45950
  return Boolean(ref.resolved?.defs.some((def) => {
44745
45951
  const node = def.node;
44746
45952
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
44747
45953
  const initializer = stripParenExpression(node.init);
44748
45954
  if (!isNodeOfType(initializer, "CallExpression")) return false;
44749
45955
  const callee = stripParenExpression(initializer.callee);
44750
- return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(callee.name);
45956
+ return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(callee.name);
44751
45957
  }));
44752
45958
  };
44753
45959
  const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
@@ -44783,16 +45989,22 @@ const noPassDataToParent = defineRule({
44783
45989
  const callExpr = getCallExpr(ref);
44784
45990
  if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
44785
45991
  const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall, isReactUseEffectCall);
44786
- if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
44787
45992
  if (!isSynchronous(ref.identifier, effectFn)) continue;
44788
45993
  const calleeNode = unwrapChainExpression(callExpr.callee);
44789
45994
  const identifier = ref.identifier;
44790
- if (callbackRefProvenance) {
44791
- if ([...callbackRefProvenance.callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
45995
+ const resolvedCallbackPropNames = isNodeOfType(calleeNode, "MemberExpression") && getStaticMemberPropertyName(calleeNode) === "current" ? null : getParentCallbackPropNames({
45996
+ analysis,
45997
+ expression: calleeNode,
45998
+ scopes: context.scopes
45999
+ });
46000
+ const callbackPropNames = callbackRefProvenance?.callbackPropNames ?? resolvedCallbackPropNames;
46001
+ if (isRefCall(analysis, ref) && !callbackPropNames) continue;
46002
+ if (callbackPropNames) {
46003
+ if ([...callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
44792
46004
  } else if (calleeNode === identifier) {
44793
46005
  const callbackPropName = getCommandCallbackPropName(analysis, identifier, isReactUseRefCall);
44794
46006
  if (callbackPropName && COMMAND_PROP_NAME_PATTERN.test(callbackPropName)) continue;
44795
- if (!isDirectParentCallbackRef(analysis, ref)) continue;
46007
+ if (!isDirectParentCallbackRef(analysis, ref, context.scopes)) continue;
44796
46008
  if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
44797
46009
  } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
44798
46010
  if (!isWholePropsObjectReference(analysis, ref)) continue;
@@ -44800,10 +46012,10 @@ const noPassDataToParent = defineRule({
44800
46012
  } else continue;
44801
46013
  const methodName = getCallMethodName(calleeNode);
44802
46014
  const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
44803
- if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
46015
+ if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead && !callbackPropNames) continue;
44804
46016
  if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
44805
- if (!callbackRefProvenance && isNamespacedApiCallee(calleeNode)) continue;
44806
- const isSetterNamedCallee = callbackRefProvenance ? [...callbackRefProvenance.callbackPropNames].every((callbackPropName) => SETTER_NAMED_PROP_PATTERN.test(callbackPropName)) : Boolean((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) && SETTER_NAMED_PROP_PATTERN.test((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) ?? ""));
46017
+ if (!callbackPropNames && isNamespacedApiCallee(calleeNode)) continue;
46018
+ const isSetterNamedCallee = callbackPropNames ? [...callbackPropNames].every((callbackPropName) => SETTER_NAMED_PROP_PATTERN.test(callbackPropName)) : Boolean((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) && SETTER_NAMED_PROP_PATTERN.test((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) ?? ""));
44807
46019
  const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
44808
46020
  const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
44809
46021
  if (isFunctionLike$1(argument)) {
@@ -44818,7 +46030,7 @@ const noPassDataToParent = defineRule({
44818
46030
  }
44819
46031
  return getDownstreamRefs(analysis, argument);
44820
46032
  }).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
44821
- if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
46033
+ if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
44822
46034
  if (!argsUpstreamRefs.some((argRef) => {
44823
46035
  if (isUseStateIdentifier(argRef.identifier)) return false;
44824
46036
  if (isExternalSubscriptionHookRef(argRef)) return false;
@@ -44856,9 +46068,47 @@ const isCallResultConsumedAsArgument = (callExpression) => {
44856
46068
  return false;
44857
46069
  };
44858
46070
  //#endregion
46071
+ //#region src/plugin/rules/state-and-effects/utils/is-custom-hook-state-result-reference.ts
46072
+ const NON_STATE_CUSTOM_HOOK_NAMES = new Set([
46073
+ "useCallbackRef",
46074
+ "useEffectEvent",
46075
+ "useEvent",
46076
+ "useEventCallback",
46077
+ "useLatest",
46078
+ "useMemoizedFn",
46079
+ "useStableCallback"
46080
+ ]);
46081
+ const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
46082
+ "useIntersectionObserver",
46083
+ "useMatchMedia",
46084
+ "useMediaJobProgress",
46085
+ "useMediaQuery",
46086
+ "useResizeObserver",
46087
+ "useVisibility",
46088
+ "useWindowSize"
46089
+ ]);
46090
+ const getHookCalleeName = (initializer) => {
46091
+ const unwrappedInitializer = stripParenExpression(initializer);
46092
+ if (!isNodeOfType(unwrappedInitializer, "CallExpression")) return null;
46093
+ const callee = stripParenExpression(unwrappedInitializer.callee);
46094
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
46095
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
46096
+ return null;
46097
+ };
46098
+ const isCustomHookStateResultReference = (analysis, reference) => Boolean(reference.resolved?.defs.some((definition) => {
46099
+ const declarator = definition.node;
46100
+ if (!isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return false;
46101
+ const calleeName = getHookCalleeName(declarator.init);
46102
+ if (!calleeName || !HOOK_NAME_PATTERN$3.test(calleeName) || BUILTIN_HOOK_NAMES.has(calleeName) || NON_STATE_CUSTOM_HOOK_NAMES.has(calleeName) || EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(calleeName)) return false;
46103
+ const initializer = stripParenExpression(declarator.init);
46104
+ if (!isNodeOfType(initializer, "CallExpression")) return false;
46105
+ return initializer.arguments.some((argument) => getDownstreamRefs(analysis, argument).some((argumentReference) => isProp(analysis, argumentReference)));
46106
+ }));
46107
+ //#endregion
44859
46108
  //#region src/plugin/rules/state-and-effects/no-pass-live-state-to-parent.ts
44860
46109
  const SETTER_NAMED_CALLBACK_PATTERN = /^set[A-Z]/;
44861
46110
  const DATA_FETCHING_CALLBACK_PATTERN = /^(fetch|refetch|load|query|request)([A-Z_]|$)/;
46111
+ const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
44862
46112
  const getCallCalleeName = (callExpr) => {
44863
46113
  if (!isNodeOfType(callExpr, "CallExpression")) return null;
44864
46114
  const callee = callExpr.callee;
@@ -44903,6 +46153,10 @@ const collectUpstreamStateRefs = (analysis, ref, stateRefs, visited) => {
44903
46153
  stateRefs.push(ref);
44904
46154
  return;
44905
46155
  }
46156
+ if (isCustomHookStateResultReference(analysis, ref)) {
46157
+ stateRefs.push(ref);
46158
+ return;
46159
+ }
44906
46160
  for (const def of ref.resolved?.defs ?? []) {
44907
46161
  if (def.type === "ImportBinding" || def.type === "Parameter") continue;
44908
46162
  const defNode = def.node;
@@ -44932,6 +46186,32 @@ const collectPropCallbackBoundStateRefs = (analysis, ref, isPropCallbackRef) =>
44932
46186
  }
44933
46187
  return stateRefs;
44934
46188
  };
46189
+ const collectDirectCallStateRefs = (analysis, callExpression) => {
46190
+ const stateReferences = [];
46191
+ for (const argument of callExpression.arguments) {
46192
+ if (isFunctionLike$1(argument)) continue;
46193
+ for (const argumentReference of getDownstreamRefs(analysis, argument)) {
46194
+ if (resolveToFunction(argumentReference)) continue;
46195
+ collectUpstreamStateRefs(analysis, argumentReference, stateReferences, /* @__PURE__ */ new Set());
46196
+ }
46197
+ }
46198
+ return stateReferences;
46199
+ };
46200
+ const getTransparentWrapperPropReference = (analysis, reference, context) => {
46201
+ for (const definition of reference.resolved?.defs ?? []) {
46202
+ const declarator = definition.node;
46203
+ if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
46204
+ const resultSymbol = context.scopes.symbolFor(declarator.id);
46205
+ const callbackArgument = getTransparentReactCallbackWrapperArgument(declarator.init, resultSymbol, context.scopes);
46206
+ if (!callbackArgument) continue;
46207
+ const callbackReferences = getDownstreamRefs(analysis, callbackArgument);
46208
+ const callbackReference = callbackReferences.find((candidateReference) => isPropCallbackInvocationRef(analysis, candidateReference));
46209
+ if (callbackReference) return callbackReference;
46210
+ const propReference = callbackReferences.find((candidateReference) => isProp(analysis, candidateReference) && !candidateReference.resolved?.references.some((candidateUsage) => candidateUsage.isWrite() && !candidateUsage.init));
46211
+ if (propReference) return propReference;
46212
+ }
46213
+ return null;
46214
+ };
44935
46215
  const isSetterNamedCallbackReceivingData = (callbackRef) => {
44936
46216
  const callExpr = getCallExpr(callbackRef);
44937
46217
  if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) return false;
@@ -44967,6 +46247,16 @@ const resolvesToLocalHookReturnBinding = (ref) => Boolean(ref?.resolved?.defs?.s
44967
46247
  const calleeName = getInitializerCalleeName(node.init);
44968
46248
  return calleeName !== null && isReactHookName(calleeName) && !FUNCTION_WRAPPER_HOOK_NAMES.has(calleeName);
44969
46249
  }));
46250
+ const getDirectLocalEffectHelper = (callExpression, effectFunction, context) => {
46251
+ const helperFunction = resolveExactLocalFunction(callExpression.callee, context.scopes);
46252
+ if (!helperFunction) return null;
46253
+ let ancestor = callExpression.parent;
46254
+ while (ancestor && ancestor !== effectFunction) {
46255
+ if (isFunctionLike$1(ancestor)) return null;
46256
+ ancestor = ancestor.parent;
46257
+ }
46258
+ return ancestor === effectFunction ? helperFunction : null;
46259
+ };
44970
46260
  const noPassLiveStateToParent = defineRule({
44971
46261
  id: "no-pass-live-state-to-parent",
44972
46262
  title: "Live state pushed to parent via effect",
@@ -44981,20 +46271,32 @@ const noPassLiveStateToParent = defineRule({
44981
46271
  if (!effectFnRefs) return;
44982
46272
  const effectFn = getEffectFn(analysis, node);
44983
46273
  if (!effectFn) return;
46274
+ const effectFunctionBody = isNodeOfType(effectFn, "ArrowFunctionExpression") || isNodeOfType(effectFn, "FunctionExpression") || isNodeOfType(effectFn, "FunctionDeclaration") ? effectFn.body : null;
44984
46275
  for (const ref of effectFnRefs) {
44985
- const propCallbackRefs = getEventualCallRefsTo(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
44986
- if (propCallbackRefs.length === 0) continue;
44987
- if (resolvesToLocalHookReturnBinding(ref)) continue;
44988
- if (!isSynchronous(ref.identifier, effectFn)) continue;
44989
46276
  const callExpr = getCallExpr(ref);
44990
- if (!callExpr) continue;
46277
+ if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
46278
+ const directLocalEffectHelper = getDirectLocalEffectHelper(callExpr, effectFn, context);
46279
+ const callGraphReferences = directLocalEffectHelper ? [ref, ...getDownstreamRefs(analysis, directLocalEffectHelper)] : [ref];
46280
+ const resolvedCallbackPropNames = getParentCallbackPropNames({
46281
+ analysis,
46282
+ expression: callExpr.callee,
46283
+ scopes: context.scopes
46284
+ });
46285
+ const callExpressionRoot = findTransparentExpressionRoot(callExpr);
46286
+ const notificationCallbackPropNames = Boolean(resolvedCallbackPropNames && callExpr.arguments.length > 0 && (!isCallResultCapturedToLocal(callExpr) || isNodeOfType(callExpressionRoot.parent, "ReturnStatement") && callExpressionRoot.parent.parent === effectFunctionBody) && [...resolvedCallbackPropNames].every((callbackPropName) => !DATA_FETCHING_CALLBACK_PATTERN.test(callbackPropName))) ? resolvedCallbackPropNames : null;
46287
+ if (!notificationCallbackPropNames && hasMutableBindingWrite(ref)) continue;
46288
+ const propCallbackRefs = callGraphReferences.flatMap((callGraphReference) => getEventualCallRefsTo(analysis, callGraphReference, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef)));
46289
+ const transparentPropReference = propCallbackRefs.length === 0 ? getTransparentWrapperPropReference(analysis, ref, context) : null;
46290
+ if (propCallbackRefs.length === 0 && !transparentPropReference && !notificationCallbackPropNames) continue;
46291
+ if (!notificationCallbackPropNames && resolvesToLocalHookReturnBinding(ref)) continue;
46292
+ if (!isSynchronous(ref.identifier, effectFn) && !directLocalEffectHelper) continue;
44991
46293
  if (isCallResultConsumedAsArgument(callExpr)) continue;
44992
46294
  const calleeNode = callExpr.callee;
44993
46295
  const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
44994
46296
  const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
44995
- if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
44996
- if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
44997
- const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
46297
+ if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead && !notificationCallbackPropNames) continue;
46298
+ if (!notificationCallbackPropNames && calleeNode && isNamespacedApiCallee(calleeNode)) continue;
46299
+ const stateArgRefs = transparentPropReference || notificationCallbackPropNames ? collectDirectCallStateRefs(analysis, callExpr) : callGraphReferences.flatMap((callGraphReference) => collectPropCallbackBoundStateRefs(analysis, callGraphReference, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef)));
44998
46300
  const handsSetterNamedCallbackData = propCallbackRefs.some(isSetterNamedCallbackReceivingData);
44999
46301
  if (stateArgRefs.length === 0 && !handsSetterNamedCallbackData) continue;
45000
46302
  context.report({
@@ -45387,6 +46689,7 @@ const isStateLikeDependency = (analysis, element, isPropName) => {
45387
46689
  if (!analysis) return true;
45388
46690
  const reference = getRef(analysis, element);
45389
46691
  if (!reference) return true;
46692
+ if (isCustomHookStateResultReference(analysis, reference)) return true;
45390
46693
  const upstreamReferences = getUpstreamRefs(analysis, reference);
45391
46694
  if (upstreamReferences.some((upstreamReference) => isState(analysis, upstreamReference))) return true;
45392
46695
  return !upstreamReferences.some((upstreamReference) => isProp(analysis, upstreamReference));
@@ -45403,6 +46706,22 @@ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
45403
46706
  if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
45404
46707
  return isPropName(callbackArgument.name) ? callbackArgument.name : null;
45405
46708
  };
46709
+ const getTransparentWrappedPropCallbackName = (callExpression, context, isPropName) => {
46710
+ const callee = stripParenExpression(callExpression.callee);
46711
+ if (!isNodeOfType(callee, "Identifier")) return null;
46712
+ const binding = findVariableInitializer(callExpression, callee.name);
46713
+ if (!binding?.initializer) return null;
46714
+ const resultSymbol = context.scopes.symbolFor(callee);
46715
+ const callbackArgument = getTransparentReactCallbackWrapperArgument(binding.initializer, resultSymbol, context.scopes);
46716
+ if (!callbackArgument) return null;
46717
+ const callbackSource = stripParenExpression(callbackArgument);
46718
+ if (isNodeOfType(callbackSource, "Identifier")) return isPropName(callbackSource.name, callbackSource) ? callbackSource.name : null;
46719
+ if (!isNodeOfType(callbackSource, "MemberExpression")) return null;
46720
+ const receiver = stripParenExpression(callbackSource.object);
46721
+ const propertyName = getStaticPropertyName(callbackSource);
46722
+ if (!isNodeOfType(receiver, "Identifier") || !propertyName) return null;
46723
+ return isPropName(receiver.name, receiver) ? propertyName : null;
46724
+ };
45406
46725
  const noPropCallbackInEffect = defineRule({
45407
46726
  id: "no-prop-callback-in-effect",
45408
46727
  title: "Parent kept in sync with a callback effect",
@@ -45436,9 +46755,16 @@ const noPropCallbackInEffect = defineRule({
45436
46755
  walkInsideStatementBlocks(callback.body, (child) => {
45437
46756
  if (!isNodeOfType(child, "CallExpression")) return;
45438
46757
  const directCallee = stripParenExpression(child.callee);
45439
- const calleeName = isNodeOfType(directCallee, "Identifier") && propStackTracker.isPropName(directCallee.name) && directCallee.name || getRefHeldPropCallbackName(child, propStackTracker.isPropName);
46758
+ const resolvedCallbackPropNames = analysis && propStackTracker.getCurrentPropNames().size > 0 ? getParentCallbackPropNames({
46759
+ analysis,
46760
+ expression: directCallee,
46761
+ scopes: context.scopes
46762
+ }) : null;
46763
+ const calleeName = resolvedCallbackPropNames && [...resolvedCallbackPropNames][0] || isNodeOfType(directCallee, "Identifier") && propStackTracker.isPropName(directCallee.name) && directCallee.name || getRefHeldPropCallbackName(child, propStackTracker.isPropName) || getTransparentWrappedPropCallbackName(child, context, propStackTracker.isPropName);
45440
46764
  if (!calleeName) return;
45441
- if (!isResultDiscardedCall(child)) return;
46765
+ const callExpressionRoot = findTransparentExpressionRoot(child);
46766
+ const isDirectEffectReturn = isNodeOfType(callExpressionRoot.parent, "ReturnStatement") && callExpressionRoot.parent.parent === callback.body;
46767
+ if (!isResultDiscardedCall(child) && !isDirectEffectReturn) return;
45442
46768
  if (reportedNodes.has(child)) return;
45443
46769
  reportedNodes.add(child);
45444
46770
  context.report({
@@ -46271,6 +47597,69 @@ const noRedundantShouldComponentUpdate = defineRule({
46271
47597
  }
46272
47598
  });
46273
47599
  //#endregion
47600
+ //#region src/plugin/rules/correctness/no-ref-callback-cleanup-before-react-19.ts
47601
+ const resolveFunctionExpressions = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
47602
+ const expression = stripParenExpression(rawExpression);
47603
+ if (isFunctionLike$1(expression)) return expression.async || expression.generator ? [] : [expression];
47604
+ if (isNodeOfType(expression, "ConditionalExpression")) {
47605
+ if (isNodeOfType(expression.test, "Literal")) return resolveFunctionExpressions(expression.test.value ? expression.consequent : expression.alternate, scopes, visitedSymbolIds);
47606
+ return [...resolveFunctionExpressions(expression.consequent, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.alternate, scopes, visitedSymbolIds)];
47607
+ }
47608
+ if (isNodeOfType(expression, "LogicalExpression")) {
47609
+ if (isNodeOfType(expression.left, "Literal")) {
47610
+ const isLeftTruthy = Boolean(expression.left.value);
47611
+ if (expression.operator === "&&" && !isLeftTruthy) return [];
47612
+ if (expression.operator === "||" && isLeftTruthy) return [];
47613
+ if (expression.operator === "??" && expression.left.value !== null) return [];
47614
+ }
47615
+ if (expression.operator === "&&") return resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds);
47616
+ return [...resolveFunctionExpressions(expression.left, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds)];
47617
+ }
47618
+ if (isNodeOfType(expression, "SequenceExpression")) {
47619
+ const finalExpression = expression.expressions.at(-1);
47620
+ return finalExpression ? resolveFunctionExpressions(finalExpression, scopes, visitedSymbolIds) : [];
47621
+ }
47622
+ if (isNodeOfType(expression, "CallExpression")) {
47623
+ if (!isReactApiCall(expression, "useCallback", scopes)) return [];
47624
+ const callback = expression.arguments[0];
47625
+ return callback && !isNodeOfType(callback, "SpreadElement") ? resolveFunctionExpressions(callback, scopes, visitedSymbolIds) : [];
47626
+ }
47627
+ if (!isNodeOfType(expression, "Identifier")) return [];
47628
+ const symbol = scopes.symbolFor(expression);
47629
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return [];
47630
+ if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration") && symbol.references.every((reference) => reference.flag === "read")) return resolveFunctionExpressions(symbol.declarationNode, scopes, new Set([...visitedSymbolIds, symbol.id]));
47631
+ const initializer = getDirectConstInitializer(symbol);
47632
+ if (!initializer) return [];
47633
+ return resolveFunctionExpressions(initializer, scopes, new Set([...visitedSymbolIds, symbol.id]));
47634
+ };
47635
+ const functionReturnsCleanupFunction = (functionExpression, scopes) => {
47636
+ if (!isFunctionLike$1(functionExpression)) return false;
47637
+ if (!isNodeOfType(functionExpression.body, "BlockStatement")) return resolveFunctionExpressions(functionExpression.body, scopes).length > 0;
47638
+ return collectFunctionReturnStatements(functionExpression).some((returnStatement) => Boolean(returnStatement.argument && resolveFunctionExpressions(returnStatement.argument, scopes).length > 0));
47639
+ };
47640
+ const callbackReturnsCleanupFunction = (callback, scopes) => {
47641
+ return resolveFunctionExpressions(callback, scopes).some((functionExpression) => functionReturnsCleanupFunction(functionExpression, scopes));
47642
+ };
47643
+ const noRefCallbackCleanupBeforeReact19 = defineRule({
47644
+ id: "no-ref-callback-cleanup-before-react-19",
47645
+ title: "Ref cleanup requires React 19",
47646
+ requires: ["react:18"],
47647
+ disabledWhen: ["react:19"],
47648
+ severity: "warn",
47649
+ recommendation: "React 18 ignores functions returned from ref callbacks. Handle cleanup when React calls the ref with `null`, or require React 19 before returning a cleanup function.",
47650
+ create: (context) => ({ JSXAttribute(node) {
47651
+ if (getJsxAttributeName(node.name) !== "ref") return;
47652
+ if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
47653
+ const callback = node.value.expression;
47654
+ if (!callback || isNodeOfType(callback, "JSXEmptyExpression")) return;
47655
+ if (!callbackReturnsCleanupFunction(callback, context.scopes)) return;
47656
+ context.report({
47657
+ node,
47658
+ message: "This ref callback returns a cleanup function, but React 18 ignores ref cleanup returns, so the cleanup never runs. Handle detachment when React calls the ref with `null`, or require React 19."
47659
+ });
47660
+ } })
47661
+ });
47662
+ //#endregion
46274
47663
  //#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
46275
47664
  const REPEATED_ANCESTOR_TYPES = new Set([
46276
47665
  "DoWhileStatement",
@@ -46866,7 +48255,7 @@ const doConditionsImplyFormula = (conditions, target) => {
46866
48255
  }
46867
48256
  return facts.didConflict || evaluateBooleanFormula$1(target, facts.assignments) === true;
46868
48257
  };
46869
- const getFunctionBindingSymbol = (functionNode, scopes) => {
48258
+ const getFunctionBindingSymbol$1 = (functionNode, scopes) => {
46870
48259
  if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.symbolFor(functionNode.id);
46871
48260
  const parent = functionNode.parent;
46872
48261
  if ((isNodeOfType(functionNode, "ArrowFunctionExpression") || isNodeOfType(functionNode, "FunctionExpression")) && isNodeOfType(parent, "VariableDeclarator") && parent.init === functionNode && isNodeOfType(parent.id, "Identifier")) return scopes.symbolFor(parent.id);
@@ -46899,7 +48288,7 @@ const isNodeEvaluatedDuringRender = (node, componentNode, scopes, visitedFunctio
46899
48288
  const synchronousCallbackCall = getSynchronousCallbackCall(functionNode);
46900
48289
  if (synchronousCallbackCall) return isNodeEvaluatedDuringRender(synchronousCallbackCall, componentNode, scopes, visitedFunctionSymbolIds);
46901
48290
  if (executesDuringRender(functionNode, scopes)) return isNodeEvaluatedDuringRender(functionNode.parent ?? functionNode, componentNode, scopes, visitedFunctionSymbolIds);
46902
- const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
48291
+ const functionSymbol = getFunctionBindingSymbol$1(functionNode, scopes);
46903
48292
  if (!functionSymbol || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
46904
48293
  visitedFunctionSymbolIds.add(functionSymbol.id);
46905
48294
  let callCount = 0;
@@ -46948,7 +48337,7 @@ const collectExposureConditions = (analysis, context, node, componentNode, prote
46948
48337
  parent = synchronousCallbackCall.parent;
46949
48338
  continue;
46950
48339
  }
46951
- const functionSymbol = getFunctionBindingSymbol(parent, context.scopes);
48340
+ const functionSymbol = getFunctionBindingSymbol$1(parent, context.scopes);
46952
48341
  if (functionSymbol?.references.length === 1) {
46953
48342
  const callExpression = isReferenceDirectlyCalled(functionSymbol.references[0].identifier);
46954
48343
  if (callExpression) {
@@ -47158,7 +48547,7 @@ const getSetterExposureConditions = (analysis, context, setterReference, compone
47158
48547
  const functionNode = findEnclosingFunction$1(setterReference.identifier);
47159
48548
  if (!functionNode) return null;
47160
48549
  if (isInlineJsxCallback(functionNode)) return [collectExposureConditions(analysis, context, functionNode, componentNode, protectedSymbolIds)];
47161
- const functionSymbol = getFunctionBindingSymbol(functionNode, context.scopes);
48550
+ const functionSymbol = getFunctionBindingSymbol$1(functionNode, context.scopes);
47162
48551
  if (!functionSymbol || functionSymbol.references.length === 0) return null;
47163
48552
  const conditionsByReference = [];
47164
48553
  for (const reference of functionSymbol.references) {
@@ -53031,12 +54420,6 @@ const isInsideEs6Component$1 = (methodDefinition) => {
53031
54420
  if (!owningClass) return false;
53032
54421
  return isPreactOrReactComponentClass(owningClass);
53033
54422
  };
53034
- const stripThisParameter = (params) => {
53035
- const first = params[0];
53036
- if (!first) return params;
53037
- if (isNodeOfType(first, "Identifier") && first.name === "this") return params.slice(1);
53038
- return params;
53039
- };
53040
54423
  const preactNoRenderArguments = defineRule({
53041
54424
  id: "preact-no-render-arguments",
53042
54425
  title: "render() reads props from arguments",
@@ -56238,8 +57621,39 @@ const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node,
56238
57621
  destructureIndex: 1
56239
57622
  });
56240
57623
  //#endregion
57624
+ //#region src/plugin/utils/unwrap-return-expression.ts
57625
+ const unwrapReturnExpression = (node) => isNodeOfType(node, "ReturnStatement") && node.argument ? node.argument : node;
57626
+ //#endregion
56241
57627
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
56242
57628
  const USE_EFFECT_ONLY = new Set(["useEffect"]);
57629
+ const USE_CALLBACK_ONLY = new Set(["useCallback"]);
57630
+ const USE_STATE_ONLY = new Set(["useState"]);
57631
+ const REACT_API_CALL_OPTIONS = {
57632
+ allowGlobalReactNamespace: true,
57633
+ allowUnboundBareCalls: true,
57634
+ resolveNamedAliases: true
57635
+ };
57636
+ const expressionReadsDerivedSymbol = (context, expression, stateDerivedSymbolIds) => {
57637
+ let readsDerivedSymbol = false;
57638
+ walkAst(expression, (node) => {
57639
+ if (readsDerivedSymbol) return false;
57640
+ if (node !== expression && isFunctionLike$1(node)) return false;
57641
+ if (isNodeOfType(node, "Identifier") && stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) readsDerivedSymbol = true;
57642
+ });
57643
+ return readsDerivedSymbol;
57644
+ };
57645
+ const getStaticObjectPropertyName = (property) => {
57646
+ if (!isNodeOfType(property, "Property") || property.computed || property.method || property.kind !== "init") return null;
57647
+ if (isNodeOfType(property.key, "Identifier")) return property.key.name;
57648
+ if (isNodeOfType(property.key, "Literal") && (typeof property.key.value === "string" || typeof property.key.value === "number")) return String(property.key.value);
57649
+ return null;
57650
+ };
57651
+ const isNonVisibleJsxSpreadProperty = (propertyName) => propertyName === "id" || propertyName.startsWith("aria-") || /^on[A-Z]/.test(propertyName);
57652
+ const isTransparentAssignmentTarget = (identifier) => {
57653
+ const expressionRoot = findTransparentExpressionRoot(identifier);
57654
+ const parent = expressionRoot.parent;
57655
+ return Boolean(isNodeOfType(parent, "AssignmentExpression") && parent.left === expressionRoot || isNodeOfType(parent, "UpdateExpression") && parent.argument === expressionRoot || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === expressionRoot);
57656
+ };
56243
57657
  const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
56244
57658
  let readsCurrent = false;
56245
57659
  walkAst(argument, (child) => {
@@ -56291,6 +57705,166 @@ const isStateUsedOnlyInIdOrAriaAttributes = (setterCall, setterName) => {
56291
57705
  });
56292
57706
  return referenceCount > 0 && !nonAriaReferenceFound;
56293
57707
  };
57708
+ const isGlobalWindowMember = (context, node, propertyName) => {
57709
+ const member = stripParenExpression(node);
57710
+ if (!isNodeOfType(member, "MemberExpression") || member.computed) return false;
57711
+ const receiver = stripParenExpression(member.object);
57712
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "window" && context.scopes.isGlobalReference(receiver) && isNodeOfType(member.property, "Identifier") && member.property.name === propertyName;
57713
+ };
57714
+ const getDirectWindowWidthSetter = (context, statement) => {
57715
+ const call = unwrapDiscardedExpression(statement);
57716
+ if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 1) return null;
57717
+ if (!isNodeOfType(call.callee, "Identifier") || !isSetterCall(call)) return null;
57718
+ const argument = call.arguments[0];
57719
+ return isGlobalWindowMember(context, argument, "innerWidth") ? call : null;
57720
+ };
57721
+ const getResizeListenerHandler = (context, statement, methodName) => {
57722
+ const call = unwrapDiscardedExpression(statement);
57723
+ if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 2) return null;
57724
+ if (!isGlobalWindowMember(context, call.callee, methodName)) return null;
57725
+ const eventName = call.arguments[0];
57726
+ const handler = call.arguments[1];
57727
+ if (!isNodeOfType(eventName, "Literal") || eventName.value !== "resize") return null;
57728
+ return isNodeOfType(handler, "Identifier") ? handler : null;
57729
+ };
57730
+ const getCleanupResizeHandler = (context, statement) => {
57731
+ if (!isNodeOfType(statement, "ReturnStatement") || !isFunctionLike$1(statement.argument)) return null;
57732
+ const cleanupStatements = getCallbackStatements(statement.argument);
57733
+ if (cleanupStatements.length !== 1) return null;
57734
+ return getResizeListenerHandler(context, unwrapReturnExpression(cleanupStatements[0]), "removeEventListener");
57735
+ };
57736
+ const findExactViewportState = (context, componentFunction, setterCall) => {
57737
+ if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
57738
+ const componentBody = componentFunction.body;
57739
+ if (!isNodeOfType(setterCall.callee, "Identifier")) return null;
57740
+ const setterSymbol = context.scopes.symbolFor(setterCall.callee);
57741
+ if (!setterSymbol || setterSymbol.kind !== "const" || !isNodeOfType(setterSymbol.declarationNode, "VariableDeclarator")) return null;
57742
+ const declarator = setterSymbol.declarationNode;
57743
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return null;
57744
+ const stateIdentifier = declarator.id.elements?.[0];
57745
+ const setterIdentifier = declarator.id.elements?.[1];
57746
+ if (!isNodeOfType(stateIdentifier, "Identifier") || !isNodeOfType(setterIdentifier, "Identifier") || setterIdentifier !== setterSymbol.bindingIdentifier || !isNodeOfType(declarator.init, "CallExpression") || !isReactApiCall(declarator.init, USE_STATE_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) return null;
57747
+ const initializer = declarator.init.arguments?.[0];
57748
+ if (!isNodeOfType(initializer, "Literal") || initializer.value !== 0) return null;
57749
+ const stateSymbol = context.scopes.symbolFor(stateIdentifier);
57750
+ if (!stateSymbol) return null;
57751
+ const stateDerivedSymbolIds = new Set([stateSymbol.id]);
57752
+ let didAddDerivedSymbol = true;
57753
+ while (didAddDerivedSymbol) {
57754
+ didAddDerivedSymbol = false;
57755
+ for (const statement of componentBody.body ?? []) {
57756
+ if (!isNodeOfType(statement, "VariableDeclaration")) continue;
57757
+ for (const candidateDeclarator of statement.declarations ?? []) {
57758
+ if (!isNodeOfType(candidateDeclarator.id, "Identifier") || !candidateDeclarator.init) continue;
57759
+ const candidateInitializer = stripParenExpression(candidateDeclarator.init);
57760
+ if (isFunctionLike$1(candidateInitializer) || isNodeOfType(candidateInitializer, "CallExpression") && isReactApiCall(candidateInitializer, USE_CALLBACK_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) continue;
57761
+ if (!expressionReadsDerivedSymbol(context, candidateInitializer, stateDerivedSymbolIds)) continue;
57762
+ const candidateSymbol = context.scopes.symbolFor(candidateDeclarator.id);
57763
+ if (candidateSymbol?.kind === "const" && candidateSymbol.references.every((reference) => reference.flag === "read" && !isTransparentAssignmentTarget(reference.identifier)) && !stateDerivedSymbolIds.has(candidateSymbol.id)) {
57764
+ stateDerivedSymbolIds.add(candidateSymbol.id);
57765
+ didAddDerivedSymbol = true;
57766
+ }
57767
+ }
57768
+ }
57769
+ }
57770
+ const staticSpreadVisibilityBySymbolId = /* @__PURE__ */ new Map();
57771
+ const hasOnlyStaticObjectReferences = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
57772
+ const symbol = context.scopes.symbolFor(identifier);
57773
+ if (!symbol) return false;
57774
+ if (visitedSymbolIds.has(symbol.id)) return true;
57775
+ const nextVisitedSymbolIds = new Set(visitedSymbolIds);
57776
+ nextVisitedSymbolIds.add(symbol.id);
57777
+ let hasUnknownReference = false;
57778
+ walkAst(componentBody, (node) => {
57779
+ if (hasUnknownReference || !isNodeOfType(node, "Identifier") || context.scopes.symbolFor(node)?.id !== symbol.id || node === symbol.bindingIdentifier) return;
57780
+ const referenceRoot = findTransparentExpressionRoot(node);
57781
+ const parent = referenceRoot.parent;
57782
+ if (isNodeOfType(parent, "JSXSpreadAttribute") && parent.argument === referenceRoot) return;
57783
+ if (isNodeOfType(parent, "VariableDeclarator") && parent.init === referenceRoot && isNodeOfType(parent.id, "Identifier") && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const" && hasOnlyStaticObjectReferences(parent.id, nextVisitedSymbolIds)) return;
57784
+ hasUnknownReference = true;
57785
+ return false;
57786
+ });
57787
+ return !hasUnknownReference;
57788
+ };
57789
+ const classifyStaticSpreadObject = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
57790
+ const symbol = context.scopes.symbolFor(identifier);
57791
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return "unknown";
57792
+ const cachedVisibility = staticSpreadVisibilityBySymbolId.get(symbol.id);
57793
+ if (cachedVisibility) return cachedVisibility;
57794
+ if (symbol.kind !== "const" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || symbol.declarationNode.id !== symbol.bindingIdentifier || !symbol.declarationNode.init) return "unknown";
57795
+ if (!hasOnlyStaticObjectReferences(identifier)) return "unknown";
57796
+ const initializer = stripParenExpression(symbol.declarationNode.init);
57797
+ const nextVisitedSymbolIds = new Set(visitedSymbolIds);
57798
+ nextVisitedSymbolIds.add(symbol.id);
57799
+ if (isNodeOfType(initializer, "Identifier")) {
57800
+ const visibility = classifyStaticSpreadObject(initializer, nextVisitedSymbolIds);
57801
+ staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
57802
+ return visibility;
57803
+ }
57804
+ if (!isNodeOfType(initializer, "ObjectExpression")) return "unknown";
57805
+ let visibility = "non-visible";
57806
+ for (const property of initializer.properties ?? []) {
57807
+ const propertyName = getStaticObjectPropertyName(property);
57808
+ if (!isNodeOfType(property, "Property") || !propertyName) {
57809
+ visibility = "unknown";
57810
+ break;
57811
+ }
57812
+ if (expressionReadsDerivedSymbol(context, property.value, stateDerivedSymbolIds) && !isNonVisibleJsxSpreadProperty(propertyName)) visibility = "visible";
57813
+ }
57814
+ staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
57815
+ return visibility;
57816
+ };
57817
+ let hasNonAriaReference = false;
57818
+ walkAst(componentBody, (node) => {
57819
+ if (hasNonAriaReference) return false;
57820
+ if (!isNodeOfType(node, "Identifier") || !stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) return;
57821
+ if (findEnclosingFunction$1(node) !== componentFunction) return;
57822
+ const parent = node.parent;
57823
+ if (parent && (isNodeOfType(parent, "MemberExpression") && parent.property === node && !parent.computed || isNodeOfType(parent, "Property") && parent.key === node && !parent.computed)) return;
57824
+ let cursor = parent;
57825
+ while (cursor && cursor !== componentBody) {
57826
+ if (isFunctionLike$1(cursor)) return;
57827
+ if (isNodeOfType(cursor, "JSXSpreadAttribute")) {
57828
+ if (isNodeOfType(node, "Identifier") && classifyStaticSpreadObject(node) === "visible") hasNonAriaReference = true;
57829
+ return;
57830
+ }
57831
+ if (isNodeOfType(cursor, "JSXAttribute")) {
57832
+ if (isEventHandlerAttribute(cursor)) return;
57833
+ if (!isInsideIdOrAriaAttribute(node)) hasNonAriaReference = true;
57834
+ return;
57835
+ }
57836
+ if (isNodeOfType(cursor, "ReturnStatement")) {
57837
+ hasNonAriaReference = true;
57838
+ return;
57839
+ }
57840
+ cursor = cursor.parent;
57841
+ }
57842
+ });
57843
+ return hasNonAriaReference ? stateIdentifier.name : null;
57844
+ };
57845
+ const isExactViewportSubscriptionEffect = (context, effectCall, callback) => {
57846
+ if (!isReactApiCall(effectCall, USE_EFFECT_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) return false;
57847
+ if (!isFunctionLike$1(callback) || callback.async || !isNodeOfType(callback.body, "BlockStatement")) return false;
57848
+ const statements = getCallbackStatements(callback);
57849
+ if (statements.length !== 4) return false;
57850
+ const handlerDeclaration = statements[0];
57851
+ if (!isNodeOfType(handlerDeclaration, "VariableDeclaration") || handlerDeclaration.kind !== "const" || handlerDeclaration.declarations?.length !== 1) return false;
57852
+ const handlerDeclarator = handlerDeclaration.declarations[0];
57853
+ if (!isNodeOfType(handlerDeclarator.id, "Identifier") || !isFunctionLike$1(handlerDeclarator.init)) return false;
57854
+ const handlerStatements = getCallbackStatements(handlerDeclarator.init);
57855
+ if (handlerStatements.length !== 1) return false;
57856
+ const handlerSetter = getDirectWindowWidthSetter(context, unwrapReturnExpression(handlerStatements[0]));
57857
+ const subscribedHandler = getResizeListenerHandler(context, statements[1], "addEventListener");
57858
+ const immediateSetter = getDirectWindowWidthSetter(context, statements[2]);
57859
+ const cleanupHandler = getCleanupResizeHandler(context, statements[3]);
57860
+ if (!handlerSetter || !subscribedHandler || !immediateSetter || !cleanupHandler) return false;
57861
+ const handlerSymbol = context.scopes.symbolFor(handlerDeclarator.id);
57862
+ if (!handlerSymbol || context.scopes.symbolFor(subscribedHandler) !== handlerSymbol || context.scopes.symbolFor(cleanupHandler) !== handlerSymbol) return false;
57863
+ if (!isNodeOfType(handlerSetter.callee, "Identifier") || !isNodeOfType(immediateSetter.callee, "Identifier") || context.scopes.symbolFor(handlerSetter.callee) !== context.scopes.symbolFor(immediateSetter.callee)) return false;
57864
+ const componentFunction = findEnclosingFunction$1(effectCall);
57865
+ if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return false;
57866
+ return findExactViewportState(context, componentFunction, immediateSetter) !== null;
57867
+ };
56294
57868
  const renderingHydrationNoFlicker = defineRule({
56295
57869
  id: "rendering-hydration-no-flicker",
56296
57870
  title: "useEffect setState flashes on mount",
@@ -56303,7 +57877,14 @@ const renderingHydrationNoFlicker = defineRule({
56303
57877
  if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
56304
57878
  const callback = getEffectCallback(node);
56305
57879
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
56306
- const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
57880
+ if (isExactViewportSubscriptionEffect(context, node, callback)) {
57881
+ context.report({
57882
+ node,
57883
+ message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
57884
+ });
57885
+ return;
57886
+ }
57887
+ const bodyStatements = getCallbackStatements(callback);
56307
57888
  if (bodyStatements.length !== 1) return;
56308
57889
  const soleStatement = bodyStatements[0];
56309
57890
  if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
@@ -64945,6 +66526,109 @@ const isDeferrableSideEffectCall = (objectName, methodName) => {
64945
66526
  if (ANALYTICS_DEFERRABLE_OBJECTS.has(objectName)) return ANALYTICS_DEFERRABLE_METHODS.has(methodName);
64946
66527
  return false;
64947
66528
  };
66529
+ const NEXT_SERVER_SOURCE = "next/server";
66530
+ const NEXT_AFTER_EXPORT_NAMES = new Set(["after", "unstable_after"]);
66531
+ const isNextAfterImportSymbol = (symbol, contextNode) => {
66532
+ if (symbol.kind !== "import") return false;
66533
+ const importBinding = getImportBindingForName(contextNode, symbol.name);
66534
+ return Boolean(importBinding && importBinding.source === NEXT_SERVER_SOURCE && !importBinding.isNamespace && importBinding.exportedName && NEXT_AFTER_EXPORT_NAMES.has(importBinding.exportedName));
66535
+ };
66536
+ const isDirectObjectPatternBinding = (symbol) => {
66537
+ if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
66538
+ if (!isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
66539
+ let bindingNode = symbol.bindingIdentifier;
66540
+ if (isNodeOfType(bindingNode.parent, "AssignmentPattern") && bindingNode.parent.left === bindingNode) bindingNode = bindingNode.parent;
66541
+ const property = bindingNode.parent;
66542
+ return Boolean(isNodeOfType(property, "Property") && property.value === bindingNode && property.parent === symbol.declarationNode.id);
66543
+ };
66544
+ const isNextServerNamespace = (expression, contextNode, scopes) => {
66545
+ let candidate = stripParenExpression(expression);
66546
+ const visitedSymbolIds = /* @__PURE__ */ new Set();
66547
+ while (isNodeOfType(candidate, "Identifier")) {
66548
+ const symbol = scopes.symbolFor(candidate);
66549
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
66550
+ if (symbol.kind === "import") {
66551
+ const importBinding = getImportBindingForName(contextNode, symbol.name);
66552
+ return Boolean(importBinding?.source === NEXT_SERVER_SOURCE && importBinding.isNamespace);
66553
+ }
66554
+ if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
66555
+ visitedSymbolIds.add(symbol.id);
66556
+ candidate = stripParenExpression(symbol.initializer);
66557
+ }
66558
+ return false;
66559
+ };
66560
+ const isNextAfterCallee = (callee, contextNode, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
66561
+ const candidate = stripParenExpression(callee);
66562
+ if (isNodeOfType(candidate, "MemberExpression")) {
66563
+ const propertyName = getStaticPropertyKeyName(candidate, { allowComputedString: true });
66564
+ return Boolean(propertyName && NEXT_AFTER_EXPORT_NAMES.has(propertyName) && isNextServerNamespace(candidate.object, contextNode, scopes));
66565
+ }
66566
+ if (!isNodeOfType(candidate, "Identifier")) return false;
66567
+ const symbol = scopes.symbolFor(candidate);
66568
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
66569
+ if (isNextAfterImportSymbol(symbol, contextNode)) return true;
66570
+ const destructuredPropertyName = getDestructuredBindingPropertyName(symbol.bindingIdentifier);
66571
+ if (symbol.kind === "const" && symbol.initializer && isDirectObjectPatternBinding(symbol) && destructuredPropertyName && NEXT_AFTER_EXPORT_NAMES.has(destructuredPropertyName)) return isNextServerNamespace(symbol.initializer, contextNode, scopes);
66572
+ if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
66573
+ visitedSymbolIds.add(symbol.id);
66574
+ return isNextAfterCallee(symbol.initializer, contextNode, scopes, visitedSymbolIds);
66575
+ };
66576
+ const getDirectArgumentCall = (expression) => {
66577
+ const expressionRoot = findTransparentExpressionRoot(expression);
66578
+ const parent = expressionRoot.parent;
66579
+ if (!isNodeOfType(parent, "CallExpression")) return null;
66580
+ return parent.arguments[0] === expressionRoot ? parent : null;
66581
+ };
66582
+ const isScheduledByNextAfter = (expression, scopes) => {
66583
+ const callExpression = getDirectArgumentCall(expression);
66584
+ return Boolean(callExpression && isNextAfterCallee(callExpression.callee, callExpression, scopes));
66585
+ };
66586
+ const getFunctionBindingSymbol = (functionNode, scopes) => {
66587
+ if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.scopeFor(functionNode).symbols.find((symbol) => symbol.declarationNode === functionNode) ?? null;
66588
+ const functionRoot = findTransparentExpressionRoot(functionNode);
66589
+ const parent = functionRoot.parent;
66590
+ if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== functionRoot || !isNodeOfType(parent.id, "Identifier")) return null;
66591
+ return scopes.symbolFor(parent.id);
66592
+ };
66593
+ const isDirectlyExported = (symbol) => {
66594
+ let declaration = symbol.declarationNode;
66595
+ if (isNodeOfType(declaration, "VariableDeclarator")) declaration = declaration.parent;
66596
+ return Boolean(declaration?.parent && (isNodeOfType(declaration.parent, "ExportNamedDeclaration") || isNodeOfType(declaration.parent, "ExportDefaultDeclaration")));
66597
+ };
66598
+ const isLexicallyInsideFunction = (node, functionNode) => {
66599
+ let enclosingFunction = findEnclosingFunction$1(node);
66600
+ while (enclosingFunction) {
66601
+ if (enclosingFunction === functionNode) return true;
66602
+ enclosingFunction = findEnclosingFunction$1(enclosingFunction);
66603
+ }
66604
+ return false;
66605
+ };
66606
+ const isExclusivelyScheduledByNextAfter = (functionNode, scopes, visitedFunctionSymbolIds) => {
66607
+ if (isScheduledByNextAfter(functionNode, scopes)) return true;
66608
+ const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
66609
+ if (!functionSymbol || isDirectlyExported(functionSymbol) || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
66610
+ const nextVisitedFunctionSymbolIds = new Set(visitedFunctionSymbolIds).add(functionSymbol.id);
66611
+ let hasAfterUse = false;
66612
+ for (const reference of functionSymbol.references) {
66613
+ if (reference.flag !== "read") return false;
66614
+ if (isLexicallyInsideFunction(reference.identifier, functionNode)) continue;
66615
+ if (isScheduledByNextAfter(reference.identifier, scopes)) {
66616
+ hasAfterUse = true;
66617
+ continue;
66618
+ }
66619
+ if (!isInsideNextAfterCallback(reference.identifier, scopes, nextVisitedFunctionSymbolIds)) return false;
66620
+ hasAfterUse = true;
66621
+ }
66622
+ return hasAfterUse;
66623
+ };
66624
+ const isInsideNextAfterCallback = (node, scopes, visitedFunctionSymbolIds = /* @__PURE__ */ new Set()) => {
66625
+ let enclosingFunction = findEnclosingFunction$1(node);
66626
+ while (enclosingFunction) {
66627
+ if (isExclusivelyScheduledByNextAfter(enclosingFunction, scopes, visitedFunctionSymbolIds)) return true;
66628
+ enclosingFunction = findEnclosingFunction$1(enclosingFunction);
66629
+ }
66630
+ return false;
66631
+ };
64948
66632
  const serverAfterNonblocking = defineRule({
64949
66633
  id: "server-after-nonblocking",
64950
66634
  title: "Blocking side effect before response",
@@ -64979,6 +66663,7 @@ const serverAfterNonblocking = defineRule({
64979
66663
  if (!objectName) return;
64980
66664
  const methodName = node.callee.property.name;
64981
66665
  if (!isDeferrableSideEffectCall(objectName, methodName)) return;
66666
+ if (isInsideNextAfterCallback(node, context.scopes)) return;
64982
66667
  context.report({
64983
66668
  node,
64984
66669
  message: `${objectName}.${methodName}() runs before the response, so your users wait longer for it.`
@@ -66031,14 +67716,6 @@ const isStateKey = (key) => {
66031
67716
  if (isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value === "state";
66032
67717
  return false;
66033
67718
  };
66034
- const findEnclosingClass = (node) => {
66035
- let ancestor = node.parent;
66036
- while (ancestor) {
66037
- if (isNodeOfType(ancestor, "ClassDeclaration") || isNodeOfType(ancestor, "ClassExpression")) return ancestor;
66038
- ancestor = ancestor.parent ?? null;
66039
- }
66040
- return null;
66041
- };
66042
67719
  const isInConstructor = (node) => {
66043
67720
  let ancestor = node.parent;
66044
67721
  while (ancestor) {
@@ -66253,17 +67930,34 @@ const stylePropObject = defineRule({
66253
67930
  };
66254
67931
  }
66255
67932
  });
67933
+ //#endregion
67934
+ //#region src/plugin/rules/security-scan/utils/has-use-server-directive-in-content.ts
67935
+ const hasUseServerDirectiveInContent = (content, relativePath = "source.tsx") => {
67936
+ const programNode = parseSourceText({
67937
+ filename: relativePath,
67938
+ sourceText: content,
67939
+ shouldAttachParentReferences: false
67940
+ });
67941
+ return programNode === null ? false : hasDirective(programNode, "use server");
67942
+ };
67943
+ //#endregion
67944
+ //#region src/plugin/rules/security-scan/supabase-client-owned-authz-field.ts
67945
+ const scanSupabaseClientOwnedAuthzField = scanByPattern({
67946
+ shouldScan: (file) => isClientSourcePath(file.relativePath),
67947
+ pattern: /\b(?:ownerId|ownerID|creatorId|creatorID|userId|userID|uid|providerId|providerID|orgId|orgID|tenantId|tenantID|teamId|teamID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin|admin)\b/,
67948
+ requireAll: [/\b(?:supabase\b|\.from\s*\(\s*["'][^"']+["']\s*\))[\s\S]{0,700}\b(?:insert|upsert|update)\s*\(\s*(?:\{|\[?\s*\{)[\s\S]{0,700}\b(?:ownerId|creatorId|userId|orgId|tenantId|role|isAdmin)\b/i],
67949
+ message: "Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS."
67950
+ });
66256
67951
  const supabaseClientOwnedAuthzField = defineRule({
66257
67952
  id: "supabase-client-owned-authz-field",
66258
67953
  title: "Client writes Supabase authorization field",
66259
67954
  severity: "error",
66260
67955
  recommendation: "Use RLS policies based on `auth.uid()` and server-owned membership rows; do not trust client-provided owner, org, or role columns.",
66261
- scan: scanByPattern({
66262
- shouldScan: (file) => isClientSourcePath(file.relativePath),
66263
- pattern: /\b(?:ownerId|ownerID|creatorId|creatorID|userId|userID|uid|providerId|providerID|orgId|orgID|tenantId|tenantID|teamId|teamID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin|admin)\b/,
66264
- requireAll: [/\b(?:supabase\b|\.from\s*\(\s*["'][^"']+["']\s*\))[\s\S]{0,700}\b(?:insert|upsert|update)\s*\(\s*(?:\{|\[?\s*\{)[\s\S]{0,700}\b(?:ownerId|creatorId|userId|orgId|tenantId|role|isAdmin)\b/i],
66265
- message: "Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS."
66266
- })
67956
+ scan: (file) => {
67957
+ const findings = scanSupabaseClientOwnedAuthzField(file);
67958
+ if (findings.length === 0) return findings;
67959
+ return hasUseServerDirectiveInContent(file.content, file.relativePath) ? [] : findings;
67960
+ }
66267
67961
  });
66268
67962
  //#endregion
66269
67963
  //#region src/plugin/rules/security-scan/utils/is-supabase-migration-path.ts
@@ -70733,6 +72427,17 @@ const reactDoctorRules = [
70733
72427
  requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
70734
72428
  }
70735
72429
  },
72430
+ {
72431
+ key: "react-doctor/no-ref-callback-cleanup-before-react-19",
72432
+ id: "no-ref-callback-cleanup-before-react-19",
72433
+ source: "react-doctor",
72434
+ originallyExternal: false,
72435
+ rule: {
72436
+ ...noRefCallbackCleanupBeforeReact19,
72437
+ framework: "global",
72438
+ category: "Bugs"
72439
+ }
72440
+ },
70736
72441
  {
70737
72442
  key: "react-doctor/no-ref-current-in-render",
70738
72443
  id: "no-ref-current-in-render",