oxlint-plugin-react-doctor 0.8.1-dev.5774353 → 0.8.1-dev.61bf03e

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 +553 -1
  2. package/dist/index.js +1378 -165
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3173,7 +3173,7 @@ const getJsxPropStaticStringValues = (attribute, scopes) => getJsxPropStaticStri
3173
3173
  const getJsxPropExhaustiveStaticStringValues = (attribute, scopes) => getJsxPropStaticStringValuesWithMode(attribute, scopes, null, true);
3174
3174
  //#endregion
3175
3175
  //#region src/plugin/rules/a11y/anchor-has-content.ts
3176
- const MESSAGE$63 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
3176
+ const MESSAGE$66 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
3177
3177
  const hasLinkRole = (roleValue) => roleValue.trim().split(/\s+/).find((roleToken) => VALID_ARIA_ROLES.has(roleToken)) === "link";
3178
3178
  const isTransComponentsTemplate = (node) => {
3179
3179
  let current = node.parent;
@@ -3215,7 +3215,7 @@ const anchorHasContent = defineRule({
3215
3215
  if (isTransComponentsTemplate(node)) return;
3216
3216
  context.report({
3217
3217
  node: opening.name,
3218
- message: MESSAGE$63
3218
+ message: MESSAGE$66
3219
3219
  });
3220
3220
  } };
3221
3221
  }
@@ -3703,7 +3703,7 @@ const parseJsxValue = (value) => {
3703
3703
  };
3704
3704
  //#endregion
3705
3705
  //#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
3706
- const MESSAGE$62 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
3706
+ const MESSAGE$65 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
3707
3707
  const ariaActivedescendantHasTabindex = defineRule({
3708
3708
  id: "aria-activedescendant-has-tabindex",
3709
3709
  title: "aria-activedescendant missing tabindex",
@@ -3721,7 +3721,7 @@ const ariaActivedescendantHasTabindex = defineRule({
3721
3721
  if (tabIndexValue === null || tabIndexValue >= -1) return;
3722
3722
  context.report({
3723
3723
  node: node.name,
3724
- message: MESSAGE$62
3724
+ message: MESSAGE$65
3725
3725
  });
3726
3726
  return;
3727
3727
  }
@@ -3729,7 +3729,7 @@ const ariaActivedescendantHasTabindex = defineRule({
3729
3729
  if (canContentEditableBeTabbable(node, context.scopes, context.settings)) return;
3730
3730
  context.report({
3731
3731
  node: node.name,
3732
- message: MESSAGE$62
3732
+ message: MESSAGE$65
3733
3733
  });
3734
3734
  } })
3735
3735
  });
@@ -6827,7 +6827,7 @@ const stripThisParameter = (parameters) => {
6827
6827
  };
6828
6828
  //#endregion
6829
6829
  //#region src/plugin/rules/security/auth-token-in-web-storage.ts
6830
- const MESSAGE$61 = "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.";
6830
+ const MESSAGE$64 = "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.";
6831
6831
  const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
6832
6832
  const STORAGE_GLOBALS = new Set([
6833
6833
  "window",
@@ -6999,7 +6999,7 @@ const authTokenInWebStorage = defineRule({
6999
6999
  })) return;
7000
7000
  context.report({
7001
7001
  node,
7002
- message: MESSAGE$61
7002
+ message: MESSAGE$64
7003
7003
  });
7004
7004
  },
7005
7005
  AssignmentExpression(node) {
@@ -7010,7 +7010,7 @@ const authTokenInWebStorage = defineRule({
7010
7010
  if (!propertyName || !isAuthCredentialKey(propertyName)) return;
7011
7011
  context.report({
7012
7012
  node: target,
7013
- message: MESSAGE$61
7013
+ message: MESSAGE$64
7014
7014
  });
7015
7015
  }
7016
7016
  }))
@@ -7149,7 +7149,7 @@ const CI_INSTALL_NEAR_SECRET_PATTERN = /(?:npm|pnpm|yarn|bun)\s+(?:install|ci)\b
7149
7149
  const INSTALL_COMMAND_PATTERN = /(?:npm|pnpm|yarn|bun)\s+(?:install|ci)\b/i;
7150
7150
  const SECRET_REFERENCE_PATTERN = /\bsecrets\.[A-Z0-9_]+/;
7151
7151
  const IGNORE_SCRIPTS_FLAG_PATTERN = /--ignore-scripts\b/;
7152
- const MESSAGE$60 = "The build or install pipeline can execute package lifecycle code while CI secrets may be present.";
7152
+ const MESSAGE$63 = "The build or install pipeline can execute package lifecycle code while CI secrets may be present.";
7153
7153
  const isWorkflowPath = (relativePath) => /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/i.test(relativePath);
7154
7154
  const scanWorkflowContent = (content) => {
7155
7155
  const lines = content.split("\n");
@@ -7194,7 +7194,7 @@ const scanWorkflowContent = (content) => {
7194
7194
  const installLineOffset = Math.max(step.lines.findIndex((stepLine) => INSTALL_COMMAND_PATTERN.test(stepLine)), 0);
7195
7195
  const installColumnIndex = step.lines[installLineOffset].search(INSTALL_COMMAND_PATTERN);
7196
7196
  return [{
7197
- message: MESSAGE$60,
7197
+ message: MESSAGE$63,
7198
7198
  line: step.startLineIndex + installLineOffset + 1,
7199
7199
  column: (installColumnIndex === -1 ? 0 : installColumnIndex) + 1
7200
7200
  }];
@@ -7204,7 +7204,7 @@ const scanWorkflowContent = (content) => {
7204
7204
  const scanNonWorkflowConfig = scanByPattern({
7205
7205
  shouldScan: (file) => isConfigOrCiPath(file.relativePath) && !file.relativePath.endsWith("package.json") && !isWorkflowPath(file.relativePath),
7206
7206
  pattern: CI_INSTALL_NEAR_SECRET_PATTERN,
7207
- message: MESSAGE$60
7207
+ message: MESSAGE$63
7208
7208
  });
7209
7209
  const scan = (file) => {
7210
7210
  if (isWorkflowPath(file.relativePath)) return scanWorkflowContent(file.content);
@@ -7954,7 +7954,7 @@ const isPureEventBlockerHandler = (attribute) => {
7954
7954
  };
7955
7955
  //#endregion
7956
7956
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
7957
- const MESSAGE$59 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
7957
+ const MESSAGE$62 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
7958
7958
  const KEY_HANDLERS = [
7959
7959
  "onKeyUp",
7960
7960
  "onKeyDown",
@@ -8165,7 +8165,7 @@ const clickEventsHaveKeyEvents = defineRule({
8165
8165
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler) || spreadEventValues.has(handler.toLowerCase()))) return;
8166
8166
  context.report({
8167
8167
  node: node.name,
8168
- message: MESSAGE$59
8168
+ message: MESSAGE$62
8169
8169
  });
8170
8170
  } };
8171
8171
  }
@@ -10243,21 +10243,19 @@ const isReactComponentName = (name) => {
10243
10243
  return firstCharacter >= 65 && firstCharacter <= 90;
10244
10244
  };
10245
10245
  //#endregion
10246
- //#region src/plugin/rules/react-ui/utils/get-class-name-literal.ts
10247
- const getClassNameLiteral = (classAttribute) => {
10248
- if (!isNodeOfType(classAttribute, "JSXAttribute")) return null;
10249
- if (!classAttribute.value) return null;
10250
- if (isNodeOfType(classAttribute.value, "Literal") && typeof classAttribute.value.value === "string") return classAttribute.value.value;
10251
- if (isNodeOfType(classAttribute.value, "JSXExpressionContainer")) {
10252
- const expression = classAttribute.value.expression;
10253
- if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
10254
- if (isNodeOfType(expression, "TemplateLiteral") && expression.quasis?.length === 1) return expression.quasis[0].value?.raw ?? null;
10255
- }
10246
+ //#region src/plugin/utils/get-jsx-attribute-static-string.ts
10247
+ const getJsxAttributeStaticString = (attribute) => {
10248
+ if (!isNodeOfType(attribute, "JSXAttribute") || !attribute.value) return null;
10249
+ if (isNodeOfType(attribute.value, "Literal") && typeof attribute.value.value === "string") return attribute.value.value;
10250
+ if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
10251
+ const expression = attribute.value.expression;
10252
+ if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
10253
+ if (isNodeOfType(expression, "TemplateLiteral") && expression.expressions.length === 0 && expression.quasis.length === 1) return expression.quasis[0].value.raw;
10256
10254
  return null;
10257
10255
  };
10258
10256
  //#endregion
10259
10257
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
10260
- const MESSAGE$58 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
10258
+ const MESSAGE$61 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
10261
10259
  const NON_OPERABLE_ELEMENTS = new Set([
10262
10260
  "td",
10263
10261
  "th",
@@ -10315,7 +10313,7 @@ const collectStaticTemplateClassTokens = (templateLiteral) => {
10315
10313
  const hasDisplayNoneClass = (opening) => {
10316
10314
  const classAttribute = hasJsxPropIgnoreCase(opening.attributes, "className") ?? hasJsxPropIgnoreCase(opening.attributes, "class");
10317
10315
  if (!classAttribute) return false;
10318
- const literalValue = getClassNameLiteral(classAttribute);
10316
+ const literalValue = getJsxAttributeStaticString(classAttribute);
10319
10317
  if (literalValue !== null) return literalValue.split(/\s+/).some(isDisplayNoneClassToken);
10320
10318
  if (classAttribute.value && isNodeOfType(classAttribute.value, "JSXExpressionContainer") && isNodeOfType(classAttribute.value.expression, "TemplateLiteral")) return collectStaticTemplateClassTokens(classAttribute.value.expression).some(isDisplayNoneClassToken);
10321
10319
  return false;
@@ -10787,7 +10785,7 @@ const controlHasAssociatedLabel = defineRule({
10787
10785
  if (candidate.enclosingBindingName !== null && labelEmbeddedNames.has(candidate.enclosingBindingName)) continue;
10788
10786
  context.report({
10789
10787
  node: candidate.opening,
10790
- message: MESSAGE$58
10788
+ message: MESSAGE$61
10791
10789
  });
10792
10790
  }
10793
10791
  }
@@ -12431,7 +12429,7 @@ const noRedundantPaddingAxes = defineRule({
12431
12429
  recommendation: "Collapse matching padding axes to `p-N` so duplicated classes do not make spacing harder to scan; keep split axes only when breakpoints differ.",
12432
12430
  create: (context) => ({ JSXAttribute(jsxAttribute) {
12433
12431
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
12434
- const classNameLiteral = getClassNameLiteral(jsxAttribute);
12432
+ const classNameLiteral = getJsxAttributeStaticString(jsxAttribute);
12435
12433
  if (!classNameLiteral) return;
12436
12434
  if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
12437
12435
  if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
@@ -12459,7 +12457,7 @@ const noRedundantSizeAxes = defineRule({
12459
12457
  return { JSXAttribute(jsxAttribute) {
12460
12458
  if (didReportInFile) return;
12461
12459
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
12462
- const classNameLiteral = getClassNameLiteral(jsxAttribute);
12460
+ const classNameLiteral = getJsxAttributeStaticString(jsxAttribute);
12463
12461
  if (!classNameLiteral) return;
12464
12462
  if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
12465
12463
  if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
@@ -12487,7 +12485,7 @@ const noSpaceOnFlexChildren = defineRule({
12487
12485
  recommendation: "Use `gap-*` on the flex or grid parent. `space-x-*` and `space-y-*` leave gaps when a child is hidden, miss spacing on wrapped lines, and don't flip in right-to-left layouts.",
12488
12486
  create: (context) => ({ JSXAttribute(jsxAttribute) {
12489
12487
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
12490
- const classNameLiteral = getClassNameLiteral(jsxAttribute);
12488
+ const classNameLiteral = getJsxAttributeStaticString(jsxAttribute);
12491
12489
  if (!classNameLiteral) return;
12492
12490
  if (!classNameLiteral.includes("space-")) return;
12493
12491
  const tokens = tokenizeClassName(classNameLiteral);
@@ -12594,7 +12592,7 @@ const noVagueButtonLabel = defineRule({
12594
12592
  });
12595
12593
  //#endregion
12596
12594
  //#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
12597
- const MESSAGE$57 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
12595
+ const MESSAGE$60 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
12598
12596
  const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
12599
12597
  const NAME_PROVIDING_ATTRIBUTES = [
12600
12598
  "aria-label",
@@ -12619,7 +12617,7 @@ const dialogHasAccessibleName = defineRule({
12619
12617
  if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
12620
12618
  context.report({
12621
12619
  node: node.name,
12622
- message: MESSAGE$57
12620
+ message: MESSAGE$60
12623
12621
  });
12624
12622
  } };
12625
12623
  }
@@ -12659,7 +12657,7 @@ const isEs6Component = (node) => {
12659
12657
  };
12660
12658
  //#endregion
12661
12659
  //#region src/plugin/rules/react-builtins/display-name.ts
12662
- const MESSAGE$56 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
12660
+ const MESSAGE$59 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
12663
12661
  const DEFAULT_ADDITIONAL_HOCS = [
12664
12662
  "observer",
12665
12663
  "lazy",
@@ -12852,7 +12850,7 @@ const displayName = defineRule({
12852
12850
  const reportAt = (node) => {
12853
12851
  context.report({
12854
12852
  node,
12855
- message: MESSAGE$56
12853
+ message: MESSAGE$59
12856
12854
  });
12857
12855
  };
12858
12856
  return {
@@ -19266,7 +19264,7 @@ const forbidElements = defineRule({
19266
19264
  });
19267
19265
  //#endregion
19268
19266
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
19269
- const MESSAGE$55 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
19267
+ const MESSAGE$58 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
19270
19268
  const forwardRefUsesRef = defineRule({
19271
19269
  id: "forward-ref-uses-ref",
19272
19270
  title: "forwardRef without ref parameter",
@@ -19289,7 +19287,7 @@ const forwardRefUsesRef = defineRule({
19289
19287
  if (isNodeOfType(onlyParam, "RestElement")) return;
19290
19288
  context.report({
19291
19289
  node: inner,
19292
- message: MESSAGE$55
19290
+ message: MESSAGE$58
19293
19291
  });
19294
19292
  } })
19295
19293
  });
@@ -19330,7 +19328,7 @@ const gitProviderUrlInjectionRisk = defineRule({
19330
19328
  });
19331
19329
  //#endregion
19332
19330
  //#region src/plugin/rules/a11y/heading-has-content.ts
19333
- const MESSAGE$54 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
19331
+ const MESSAGE$57 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
19334
19332
  const DEFAULT_HEADING_TAGS = [
19335
19333
  "h1",
19336
19334
  "h2",
@@ -19364,7 +19362,7 @@ const headingHasContent = defineRule({
19364
19362
  for (const attribute of ["aria-label", "aria-labelledby"]) if (hasJsxPropIgnoreCase(node.attributes, attribute)) return;
19365
19363
  context.report({
19366
19364
  node,
19367
- message: MESSAGE$54
19365
+ message: MESSAGE$57
19368
19366
  });
19369
19367
  } };
19370
19368
  }
@@ -19532,7 +19530,7 @@ const hooksNoNanInDeps = defineRule({
19532
19530
  });
19533
19531
  //#endregion
19534
19532
  //#region src/plugin/rules/a11y/html-has-lang.ts
19535
- const MESSAGE$53 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
19533
+ const MESSAGE$56 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
19536
19534
  const resolveSettings$38 = (settings) => {
19537
19535
  const reactDoctor = settings?.["react-doctor"];
19538
19536
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -19579,13 +19577,13 @@ const htmlHasLang = defineRule({
19579
19577
  if (!lang) {
19580
19578
  context.report({
19581
19579
  node: node.name,
19582
- message: MESSAGE$53
19580
+ message: MESSAGE$56
19583
19581
  });
19584
19582
  return;
19585
19583
  }
19586
19584
  if (evaluateLang(lang.value) === "empty") context.report({
19587
19585
  node: lang,
19588
- message: MESSAGE$53
19586
+ message: MESSAGE$56
19589
19587
  });
19590
19588
  } };
19591
19589
  }
@@ -19858,7 +19856,7 @@ const isJsxFragmentElement = (node, scopes) => {
19858
19856
  };
19859
19857
  //#endregion
19860
19858
  //#region src/plugin/rules/a11y/iframe-has-title.ts
19861
- const MESSAGE$52 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
19859
+ const MESSAGE$55 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
19862
19860
  const isStaticallyAriaHidden = (openingElement) => {
19863
19861
  const ariaHiddenAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "aria-hidden", false);
19864
19862
  if (!ariaHiddenAttribute) return false;
@@ -19947,14 +19945,14 @@ const iframeHasTitle = defineRule({
19947
19945
  if (!titleAttr) {
19948
19946
  if (hasSpread || tag === "iframe") context.report({
19949
19947
  node: node.name,
19950
- message: MESSAGE$52
19948
+ message: MESSAGE$55
19951
19949
  });
19952
19950
  return;
19953
19951
  }
19954
19952
  const verdict = evaluateTitleValue(titleAttr.value);
19955
19953
  if (verdict === "missing" || verdict === "empty") context.report({
19956
19954
  node: titleAttr,
19957
- message: MESSAGE$52
19955
+ message: MESSAGE$55
19958
19956
  });
19959
19957
  } })
19960
19958
  });
@@ -20079,7 +20077,7 @@ const iframeMissingSandbox = defineRule({
20079
20077
  });
20080
20078
  //#endregion
20081
20079
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
20082
- const MESSAGE$51 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
20080
+ const MESSAGE$54 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
20083
20081
  const DEFAULT_COMPONENTS = ["img"];
20084
20082
  const DEFAULT_REDUNDANT_WORDS = [
20085
20083
  "image",
@@ -20144,7 +20142,7 @@ const imgRedundantAlt = defineRule({
20144
20142
  if (!altAttribute) return;
20145
20143
  if (altValueRedundant(altAttribute, settings.words)) context.report({
20146
20144
  node: altAttribute,
20147
- message: MESSAGE$51
20145
+ message: MESSAGE$54
20148
20146
  });
20149
20147
  } };
20150
20148
  }
@@ -21797,7 +21795,7 @@ const isDiscardedProbeInsideTry = (node) => {
21797
21795
  return false;
21798
21796
  };
21799
21797
  const isComponentOrHookName = (name) => /^[A-Z]/.test(name) || /^use[A-Z0-9]/.test(name);
21800
- const getFunctionName = (functionNode) => {
21798
+ const getFunctionName$1 = (functionNode) => {
21801
21799
  if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id.name;
21802
21800
  const holder = functionNode.parent;
21803
21801
  if (holder && isNodeOfType(holder, "VariableDeclarator") && isNodeOfType(holder.id, "Identifier")) return holder.id.name;
@@ -21806,7 +21804,7 @@ const getFunctionName = (functionNode) => {
21806
21804
  const isUncacheableOptionsMergeUtility = (node) => {
21807
21805
  const enclosingFunction = findEnclosingFunction$1(node);
21808
21806
  if (!enclosingFunction || !isFunctionLike$1(enclosingFunction)) return false;
21809
- const functionName = getFunctionName(enclosingFunction);
21807
+ const functionName = getFunctionName$1(enclosingFunction);
21810
21808
  if (!functionName || isComponentOrHookName(functionName)) return false;
21811
21809
  const parameterNames = /* @__PURE__ */ new Set();
21812
21810
  for (const parameter of enclosingFunction.params ?? []) collectPatternNames(parameter, parameterNames);
@@ -22108,7 +22106,7 @@ const getHoistableRegExpConstructionKind = (node, context) => {
22108
22106
  if (!STATEFUL_REGEXP_FLAGS_PATTERN.test(effectiveFlags)) return "stateless";
22109
22107
  return isSafeStatefulReplaceAllSearch(node, effectiveFlags, context) ? "statefulReplaceAll" : null;
22110
22108
  };
22111
- const MESSAGE$50 = "`new RegExp()` rebuilds the pattern on every loop pass. Move it to a constant outside the loop.";
22109
+ const MESSAGE$53 = "`new RegExp()` rebuilds the pattern on every loop pass. Move it to a constant outside the loop.";
22112
22110
  const jsHoistRegexp = defineRule({
22113
22111
  id: "js-hoist-regexp",
22114
22112
  title: "RegExp built inside a loop",
@@ -22125,7 +22123,7 @@ const jsHoistRegexp = defineRule({
22125
22123
  if (constructionKind === "statefulReplaceAll" && cachedEnvironmentHazard === "replaceAllIntegrityLost") return;
22126
22124
  context.report({
22127
22125
  node,
22128
- message: MESSAGE$50
22126
+ message: MESSAGE$53
22129
22127
  });
22130
22128
  };
22131
22129
  return createLoopAwareVisitors({
@@ -24892,7 +24890,7 @@ const jsxMaxDepth = defineRule({
24892
24890
  });
24893
24891
  //#endregion
24894
24892
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
24895
- const MESSAGE$49 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
24893
+ const MESSAGE$52 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
24896
24894
  const LITERAL_TEXT_TAGS = new Set([
24897
24895
  "code",
24898
24896
  "pre",
@@ -24962,7 +24960,7 @@ const jsxNoCommentTextnodes = defineRule({
24962
24960
  if (isDeliberateStyledCommentToken(node)) return;
24963
24961
  context.report({
24964
24962
  node,
24965
- message: MESSAGE$49
24963
+ message: MESSAGE$52
24966
24964
  });
24967
24965
  } })
24968
24966
  });
@@ -24993,7 +24991,7 @@ const isInsideFunctionScope = (node) => {
24993
24991
  };
24994
24992
  //#endregion
24995
24993
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
24996
- const MESSAGE$48 = "Every reader of this context redraws on each render because you build its `value` inline.";
24994
+ const MESSAGE$51 = "Every reader of this context redraws on each render because you build its `value` inline.";
24997
24995
  const CONTEXT_MODULES$1 = [
24998
24996
  "react",
24999
24997
  "use-context-selector",
@@ -25096,7 +25094,7 @@ const jsxNoConstructedContextValues = defineRule({
25096
25094
  if (!isConstructedValue(innerExpression)) continue;
25097
25095
  context.report({
25098
25096
  node: attribute,
25099
- message: MESSAGE$48
25097
+ message: MESSAGE$51
25100
25098
  });
25101
25099
  }
25102
25100
  }
@@ -25962,7 +25960,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
25962
25960
  ];
25963
25961
  //#endregion
25964
25962
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
25965
- const MESSAGE$47 = "This child redraws every render because the prop gets a brand new array each time.";
25963
+ const MESSAGE$50 = "This child redraws every render because the prop gets a brand new array each time.";
25966
25964
  const isDataArrayPropName = (propName) => {
25967
25965
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
25968
25966
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -26049,7 +26047,7 @@ const jsxNoNewArrayAsProp = defineRule({
26049
26047
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
26050
26048
  context.report({
26051
26049
  node,
26052
- message: MESSAGE$47
26050
+ message: MESSAGE$50
26053
26051
  });
26054
26052
  }
26055
26053
  };
@@ -26307,7 +26305,7 @@ const SAFE_RECEIVER_NAMES = new Set([
26307
26305
  ]);
26308
26306
  //#endregion
26309
26307
  //#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
26310
- const MESSAGE$46 = "This child redraws every render because the prop gets a brand new function each time.";
26308
+ const MESSAGE$49 = "This child redraws every render because the prop gets a brand new function each time.";
26311
26309
  const isAccessorPredicateName = (propName) => {
26312
26310
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
26313
26311
  if (propName.length <= prefix.length) continue;
@@ -26514,7 +26512,7 @@ const jsxNoNewFunctionAsProp = defineRule({
26514
26512
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
26515
26513
  context.report({
26516
26514
  node,
26517
- message: MESSAGE$46
26515
+ message: MESSAGE$49
26518
26516
  });
26519
26517
  }
26520
26518
  };
@@ -26734,7 +26732,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
26734
26732
  ];
26735
26733
  //#endregion
26736
26734
  //#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
26737
- const MESSAGE$45 = "This child redraws every render because the prop gets a brand new object each time.";
26735
+ const MESSAGE$48 = "This child redraws every render because the prop gets a brand new object each time.";
26738
26736
  const isConfigObjectPropName = (propName) => {
26739
26737
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
26740
26738
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -26823,7 +26821,7 @@ const jsxNoNewObjectAsProp = defineRule({
26823
26821
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
26824
26822
  context.report({
26825
26823
  node,
26826
- message: MESSAGE$45
26824
+ message: MESSAGE$48
26827
26825
  });
26828
26826
  }
26829
26827
  };
@@ -26831,7 +26829,7 @@ const jsxNoNewObjectAsProp = defineRule({
26831
26829
  });
26832
26830
  //#endregion
26833
26831
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
26834
- const MESSAGE$44 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
26832
+ const MESSAGE$47 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
26835
26833
  const JAVASCRIPT_URL_PATTERN = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
26836
26834
  const resolveSettings$28 = (settings) => {
26837
26835
  const reactDoctor = settings?.["react-doctor"];
@@ -26869,7 +26867,7 @@ const jsxNoScriptUrl = defineRule({
26869
26867
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
26870
26868
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
26871
26869
  node: attribute,
26872
- message: MESSAGE$44
26870
+ message: MESSAGE$47
26873
26871
  });
26874
26872
  }
26875
26873
  } };
@@ -27212,7 +27210,7 @@ const jsxPropsNoSpreadMulti = defineRule({
27212
27210
  });
27213
27211
  //#endregion
27214
27212
  //#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
27215
- const MESSAGE$43 = "You can't tell what props reach this element when you spread them.";
27213
+ const MESSAGE$46 = "You can't tell what props reach this element when you spread them.";
27216
27214
  const resolveSettings$25 = (settings) => {
27217
27215
  const reactDoctor = settings?.["react-doctor"];
27218
27216
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -27255,7 +27253,7 @@ const jsxPropsNoSpreading = defineRule({
27255
27253
  }
27256
27254
  context.report({
27257
27255
  node: attribute,
27258
- message: MESSAGE$43
27256
+ message: MESSAGE$46
27259
27257
  });
27260
27258
  didReportInFile = true;
27261
27259
  return;
@@ -27531,7 +27529,7 @@ const labelHasAssociatedControl = defineRule({
27531
27529
  });
27532
27530
  //#endregion
27533
27531
  //#region src/plugin/rules/a11y/lang.ts
27534
- const MESSAGE$42 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
27532
+ const MESSAGE$45 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
27535
27533
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
27536
27534
  "aa",
27537
27535
  "ab",
@@ -28011,7 +28009,7 @@ const lang = defineRule({
28011
28009
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
28012
28010
  context.report({
28013
28011
  node: langAttr,
28014
- message: MESSAGE$42
28012
+ message: MESSAGE$45
28015
28013
  });
28016
28014
  return;
28017
28015
  }
@@ -28020,7 +28018,7 @@ const lang = defineRule({
28020
28018
  if (value === null) return;
28021
28019
  if (!isValidLangTag(value)) context.report({
28022
28020
  node: langAttr,
28023
- message: MESSAGE$42
28021
+ message: MESSAGE$45
28024
28022
  });
28025
28023
  } })
28026
28024
  });
@@ -28071,7 +28069,7 @@ const mdxSsrExecutionRisk = defineRule({
28071
28069
  });
28072
28070
  //#endregion
28073
28071
  //#region src/plugin/rules/a11y/media-has-caption.ts
28074
- const MESSAGE$41 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
28072
+ const MESSAGE$44 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
28075
28073
  const DEFAULT_AUDIO = ["audio"];
28076
28074
  const DEFAULT_VIDEO = ["video"];
28077
28075
  const DEFAULT_TRACK = ["track"];
@@ -28161,7 +28159,7 @@ const mediaHasCaption = defineRule({
28161
28159
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
28162
28160
  context.report({
28163
28161
  node: node.name,
28164
- message: MESSAGE$41
28162
+ message: MESSAGE$44
28165
28163
  });
28166
28164
  return;
28167
28165
  }
@@ -28180,12 +28178,553 @@ const mediaHasCaption = defineRule({
28180
28178
  return kindValue.value.toLowerCase() === "captions";
28181
28179
  })) context.report({
28182
28180
  node: node.name,
28183
- message: MESSAGE$41
28181
+ message: MESSAGE$44
28184
28182
  });
28185
28183
  } };
28186
28184
  }
28187
28185
  });
28188
28186
  //#endregion
28187
+ //#region src/plugin/utils/get-class-binding-symbol.ts
28188
+ const getClassBindingSymbol = (classNode, scopes) => {
28189
+ if (isNodeOfType(classNode.id, "Identifier")) return scopes.symbolFor(classNode.id);
28190
+ const parent = classNode.parent;
28191
+ return isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier") ? scopes.symbolFor(parent.id) : null;
28192
+ };
28193
+ //#endregion
28194
+ //#region src/plugin/utils/mobx-rule-gates.ts
28195
+ const MOBX_RULE_GATES = {
28196
+ "mobx-reaction-disposer-discarded": { requires: ["mobx:4"] },
28197
+ "mobx-no-make-auto-observable-in-inheritance": { requires: ["mobx:6"] },
28198
+ "mobx-no-computed-side-effects": { requires: ["mobx:4"] },
28199
+ "mobx-async-action-requires-action": { requires: ["mobx:4"] },
28200
+ "mobx-no-observer-wrapped-memo": { requires: [
28201
+ "mobx:4",
28202
+ "mobx-react-binding",
28203
+ "react"
28204
+ ] },
28205
+ "mobx-make-observable-unconditional": { requires: ["mobx:6"] },
28206
+ "mobx-legacy-decorator-needs-make-observable": { requires: ["mobx:6"] },
28207
+ "mobx-initialize-before-make-auto-observable": { requires: ["mobx:6"] },
28208
+ "mobx-observable-read-needs-observer": {
28209
+ requires: [
28210
+ "mobx:4",
28211
+ "mobx-react-binding",
28212
+ "react"
28213
+ ],
28214
+ disabledWhen: ["mobx-react-observer"]
28215
+ },
28216
+ "mobx-observer-before-inject": { requires: [
28217
+ "mobx:4",
28218
+ "mobx-react",
28219
+ "react"
28220
+ ] },
28221
+ "mobx-reaction-requires-observable": { requires: ["mobx:4"] },
28222
+ "mobx-no-invalid-observable-override": { requires: ["mobx:6"] },
28223
+ "mobx-no-observable-prop-to-untracked-child": { requires: [
28224
+ "mobx:4",
28225
+ "mobx-react-binding",
28226
+ "react"
28227
+ ] },
28228
+ "mobx-no-stale-observable-snapshot-after-await": { requires: ["mobx:4"] },
28229
+ "mobx-no-reaction-comparison-value-mutation": { requires: ["mobx:4"] },
28230
+ "mobx-observer-class-no-should-component-update": { requires: [
28231
+ "mobx:4",
28232
+ "mobx-react",
28233
+ "react"
28234
+ ] },
28235
+ "mobx-enable-static-rendering-for-ssr": { requires: [
28236
+ "mobx:4",
28237
+ "mobx-react-binding",
28238
+ "react",
28239
+ "ssr"
28240
+ ] },
28241
+ "mobx-no-rest-destructure-observable": { requires: [
28242
+ "mobx:4",
28243
+ "mobx-react-binding",
28244
+ "react"
28245
+ ] },
28246
+ "mobx-computed-depends-on-non-observable": { requires: ["mobx:4"] },
28247
+ "mobx-no-keepalive-computed-without-disposal": { requires: ["mobx:4"] }
28248
+ };
28249
+ //#endregion
28250
+ //#region src/plugin/utils/resolve-imported-api-reference.ts
28251
+ const resolveImportSymbol = (symbol) => {
28252
+ const importDeclaration = getImportDeclarationForSymbol(symbol);
28253
+ if (!importDeclaration || typeof importDeclaration.source.value !== "string") return null;
28254
+ if (isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")) return {
28255
+ source: importDeclaration.source.value,
28256
+ importedName: null,
28257
+ isNamespace: true
28258
+ };
28259
+ if (isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier")) return {
28260
+ source: importDeclaration.source.value,
28261
+ importedName: "default",
28262
+ isNamespace: false
28263
+ };
28264
+ const importedName = getImportedName(symbol.declarationNode);
28265
+ return importedName ? {
28266
+ source: importDeclaration.source.value,
28267
+ importedName,
28268
+ isNamespace: false
28269
+ } : null;
28270
+ };
28271
+ const resolveIdentifierReference = (identifier, scopes, visitedSymbolIds) => {
28272
+ const symbol = scopes.symbolFor(identifier);
28273
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return null;
28274
+ if (symbol.kind === "import") return resolveImportSymbol(symbol);
28275
+ if (symbol.kind !== "const" || !symbol.initializer) return null;
28276
+ visitedSymbolIds.add(symbol.id);
28277
+ if (isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) {
28278
+ const receiver = resolveImportedApiReference(symbol.initializer, scopes, visitedSymbolIds);
28279
+ if (!receiver || !receiver.isNamespace && receiver.importedName !== "default") return null;
28280
+ for (const property of symbol.declarationNode.id.properties) {
28281
+ if (!isNodeOfType(property, "Property")) continue;
28282
+ if ((isNodeOfType(property.value, "AssignmentPattern") ? property.value.left : property.value) !== symbol.bindingIdentifier) continue;
28283
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
28284
+ return propertyName ? {
28285
+ source: receiver.source,
28286
+ importedName: propertyName,
28287
+ isNamespace: false
28288
+ } : null;
28289
+ }
28290
+ return null;
28291
+ }
28292
+ return resolveImportedApiReference(symbol.initializer, scopes, visitedSymbolIds);
28293
+ };
28294
+ const resolveImportedApiReference = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
28295
+ const unwrappedExpression = stripParenExpression(expression);
28296
+ if (isNodeOfType(unwrappedExpression, "Identifier")) return resolveIdentifierReference(unwrappedExpression, scopes, visitedSymbolIds);
28297
+ if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
28298
+ const propertyName = getStaticPropertyName(unwrappedExpression);
28299
+ if (!propertyName) return null;
28300
+ const receiver = resolveImportedApiReference(unwrappedExpression.object, scopes, visitedSymbolIds);
28301
+ if (!receiver || !receiver.isNamespace && receiver.importedName !== "default") return null;
28302
+ return {
28303
+ source: receiver.source,
28304
+ importedName: propertyName,
28305
+ isNamespace: false
28306
+ };
28307
+ };
28308
+ //#endregion
28309
+ //#region src/plugin/rules/mobx/mobx-no-make-auto-observable-in-inheritance.ts
28310
+ const MESSAGE$43 = "MobX does not support `makeAutoObservable(this)` in inherited classes. Use composition or explicit `makeObservable` annotations.";
28311
+ const getEnclosingConstructorClass = (node) => {
28312
+ let ancestor = node.parent;
28313
+ while (ancestor) {
28314
+ if (!isFunctionLike$1(ancestor)) {
28315
+ ancestor = ancestor.parent ?? null;
28316
+ continue;
28317
+ }
28318
+ const methodDefinition = ancestor.parent;
28319
+ if (!isNodeOfType(methodDefinition, "MethodDefinition") || methodDefinition.kind !== "constructor") return null;
28320
+ const classNode = methodDefinition.parent?.parent;
28321
+ return isNodeOfType(classNode, "ClassDeclaration") || isNodeOfType(classNode, "ClassExpression") ? classNode : null;
28322
+ }
28323
+ return null;
28324
+ };
28325
+ const isNonNullSuperclass = (classNode) => {
28326
+ if (!classNode.superClass) return false;
28327
+ const superClass = stripParenExpression(classNode.superClass);
28328
+ return !(isNodeOfType(superClass, "Literal") && superClass.value === null);
28329
+ };
28330
+ const subclassedClassSymbolIdsByAnalysis = /* @__PURE__ */ new WeakMap();
28331
+ const getSubclassedClassSymbolIds = (node, scopes) => {
28332
+ const cached = subclassedClassSymbolIdsByAnalysis.get(scopes);
28333
+ if (cached) return cached;
28334
+ const symbolIds = /* @__PURE__ */ new Set();
28335
+ const program = findProgramRoot(node);
28336
+ if (program) walkAst(program, (candidate) => {
28337
+ if (!isNodeOfType(candidate, "ClassDeclaration") && !isNodeOfType(candidate, "ClassExpression") || !isNonNullSuperclass(candidate)) return;
28338
+ const superClassExpression = candidate.superClass;
28339
+ if (!superClassExpression) return;
28340
+ const superClass = stripParenExpression(superClassExpression);
28341
+ if (!isNodeOfType(superClass, "Identifier")) return;
28342
+ const symbol = resolveConstIdentifierAlias(superClass, scopes);
28343
+ if (symbol?.kind === "class" || symbol?.kind === "const" && isNodeOfType(symbol.initializer, "ClassExpression")) symbolIds.add(symbol.id);
28344
+ });
28345
+ subclassedClassSymbolIdsByAnalysis.set(scopes, symbolIds);
28346
+ return symbolIds;
28347
+ };
28348
+ const isMakeAutoObservableCall = (callExpression, scopes) => {
28349
+ const reference = resolveImportedApiReference(callExpression.callee, scopes);
28350
+ return reference?.source === "mobx" && reference.importedName === "makeAutoObservable";
28351
+ };
28352
+ const mobxNoMakeAutoObservableInInheritance = defineRule({
28353
+ id: "mobx-no-make-auto-observable-in-inheritance",
28354
+ title: "Unsupported MobX auto-observable inheritance",
28355
+ severity: "error",
28356
+ category: "Bugs",
28357
+ requires: MOBX_RULE_GATES["mobx-no-make-auto-observable-in-inheritance"].requires,
28358
+ recommendation: "Replace inheritance with composition, or annotate inherited members explicitly with `makeObservable`.",
28359
+ create: (context) => ({ CallExpression(callExpression) {
28360
+ if (!isMakeAutoObservableCall(callExpression, context.scopes)) return;
28361
+ const target = callExpression.arguments[0];
28362
+ if (!target || !isNodeOfType(stripParenExpression(target), "ThisExpression")) return;
28363
+ const classNode = getEnclosingConstructorClass(callExpression);
28364
+ if (!classNode) return;
28365
+ const classSymbol = getClassBindingSymbol(classNode, context.scopes);
28366
+ const isSubclassed = Boolean(classSymbol && getSubclassedClassSymbolIds(callExpression, context.scopes).has(classSymbol.id));
28367
+ if (!isNonNullSuperclass(classNode) && !isSubclassed) return;
28368
+ context.report({
28369
+ node: callExpression,
28370
+ message: MESSAGE$43
28371
+ });
28372
+ } })
28373
+ });
28374
+ //#endregion
28375
+ //#region src/plugin/rules/mobx/mobx-no-observer-wrapped-memo.ts
28376
+ const OBSERVER_MODULES = new Set(["mobx-react", "mobx-react-lite"]);
28377
+ const MESSAGE$42 = "`observer` cannot wrap an already memoized or observed component. Apply `observer` first, then place `memo` outside only if needed.";
28378
+ const isObserverCall = (callExpression, scopes) => {
28379
+ const reference = resolveImportedApiReference(callExpression.callee, scopes);
28380
+ return Boolean(reference?.importedName === "observer" && OBSERVER_MODULES.has(reference.source));
28381
+ };
28382
+ const hasInvalidInnerWrapper = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
28383
+ const unwrappedExpression = stripParenExpression(expression);
28384
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
28385
+ const symbol = scopes.symbolFor(unwrappedExpression);
28386
+ if (symbol?.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return false;
28387
+ visitedSymbolIds.add(symbol.id);
28388
+ return hasInvalidInnerWrapper(symbol.initializer, scopes, visitedSymbolIds);
28389
+ }
28390
+ if (!isNodeOfType(unwrappedExpression, "CallExpression")) return false;
28391
+ const reference = resolveImportedApiReference(unwrappedExpression.callee, scopes);
28392
+ if (reference?.importedName === "observer" && OBSERVER_MODULES.has(reference.source)) return true;
28393
+ return reference?.source === "react" && reference.importedName === "memo";
28394
+ };
28395
+ const mobxNoObserverWrappedMemo = defineRule({
28396
+ id: "mobx-no-observer-wrapped-memo",
28397
+ title: "Invalid MobX observer wrapper order",
28398
+ severity: "error",
28399
+ category: "Bugs",
28400
+ requires: MOBX_RULE_GATES["mobx-no-observer-wrapped-memo"].requires,
28401
+ recommendation: "Pass the component directly to `observer`, or apply React `memo` outside the resulting observer component.",
28402
+ create: (context) => ({ CallExpression(callExpression) {
28403
+ if (!isObserverCall(callExpression, context.scopes)) return;
28404
+ const componentArgument = callExpression.arguments[0];
28405
+ if (!componentArgument) return;
28406
+ if (!hasInvalidInnerWrapper(componentArgument, context.scopes)) return;
28407
+ context.report({
28408
+ node: callExpression,
28409
+ message: MESSAGE$42
28410
+ });
28411
+ } })
28412
+ });
28413
+ //#endregion
28414
+ //#region src/plugin/utils/is-result-discarded-call.ts
28415
+ const isResultDiscardedCall = (callExpression) => {
28416
+ let node = callExpression;
28417
+ let parent = node.parent;
28418
+ while (parent) {
28419
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
28420
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
28421
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
28422
+ if (isNodeOfType(parent, "ChainExpression")) {
28423
+ node = parent;
28424
+ parent = node.parent;
28425
+ continue;
28426
+ }
28427
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
28428
+ node = parent;
28429
+ parent = node.parent;
28430
+ continue;
28431
+ }
28432
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
28433
+ node = parent;
28434
+ parent = node.parent;
28435
+ continue;
28436
+ }
28437
+ if (isNodeOfType(parent, "SequenceExpression")) {
28438
+ const expressions = parent.expressions ?? [];
28439
+ if (expressions[expressions.length - 1] !== node) return true;
28440
+ node = parent;
28441
+ parent = node.parent;
28442
+ continue;
28443
+ }
28444
+ return false;
28445
+ }
28446
+ return false;
28447
+ };
28448
+ //#endregion
28449
+ //#region src/plugin/utils/resolve-stable-options-object.ts
28450
+ const resolveStableOptionsObject = (expression, observedPropertyNames, scopes, referenceNode = expression, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
28451
+ const unwrappedExpression = stripParenExpression(expression);
28452
+ if (isNodeOfType(unwrappedExpression, "ObjectExpression")) return unwrappedExpression;
28453
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
28454
+ const symbol = scopes.symbolFor(unwrappedExpression);
28455
+ if (!symbol?.initializer || visitedSymbolIds.has(symbol.id) || hasSymbolWriteBefore(symbol, referenceNode, scopes) || observedPropertyNames.some((propertyName) => hasPossibleStaticPropertyWriteBefore(unwrappedExpression, propertyName, referenceNode, scopes))) return null;
28456
+ visitedSymbolIds.add(symbol.id);
28457
+ return resolveStableOptionsObject(symbol.initializer, observedPropertyNames, scopes, referenceNode, visitedSymbolIds);
28458
+ };
28459
+ //#endregion
28460
+ //#region src/plugin/rules/mobx/mobx-reaction-disposer-discarded.ts
28461
+ const MESSAGE$41 = "This MobX reaction discards its disposer and can outlive its owner. Store and dispose it during teardown, or provide an AbortSignal.";
28462
+ const LEAKING_SUBSCRIPTION_NAMES = new Set(["reaction", "autorun"]);
28463
+ const OPTIONS_ARGUMENT_INDEX = {
28464
+ autorun: 1,
28465
+ reaction: 2
28466
+ };
28467
+ const DISPOSER_COERCION_NAMES = new Set([
28468
+ "Boolean",
28469
+ "Number",
28470
+ "String"
28471
+ ]);
28472
+ const NON_OBSERVABLE_GLOBAL_RECEIVER_NAMES = new Set([
28473
+ "Array",
28474
+ "Boolean",
28475
+ "JSON",
28476
+ "Math",
28477
+ "Number",
28478
+ "Object",
28479
+ "String",
28480
+ "console"
28481
+ ]);
28482
+ const NON_OBSERVING_IMPORTED_METHOD_NAMES = new Set([
28483
+ "add",
28484
+ "clear",
28485
+ "delete",
28486
+ "remove",
28487
+ "save",
28488
+ "set",
28489
+ "update",
28490
+ "write"
28491
+ ]);
28492
+ const REACTION_PARAMETER_INDEX = {
28493
+ autorun: 0,
28494
+ reaction: 2
28495
+ };
28496
+ const REACTION_CALLBACK_INDEX = {
28497
+ autorun: 0,
28498
+ reaction: 1
28499
+ };
28500
+ const OBSERVATION_CALLBACK_INDEX = {
28501
+ autorun: 0,
28502
+ reaction: 0
28503
+ };
28504
+ const PROCESS_LIFETIME_WIRING_NAME_PATTERN = /^(?:register.*(?:reactions?|autoruns?)|init.*(?:stores?|reactions?|autoruns?)|setup.*(?:stores?|reactions?|autoruns?)|bootstrap(?:app(?:lication)?|stores?|reactions?|autoruns?))$/i;
28505
+ const resolveLeakingSubscriptionName = (callExpression, scopes) => {
28506
+ const reference = resolveImportedApiReference(callExpression.callee, scopes);
28507
+ if (reference?.source !== "mobx" || !reference.importedName || !LEAKING_SUBSCRIPTION_NAMES.has(reference.importedName)) return null;
28508
+ return reference.importedName;
28509
+ };
28510
+ const isEvaluatedAtModuleScope = (node) => {
28511
+ let ancestor = node.parent;
28512
+ while (ancestor) {
28513
+ if (isNodeOfType(ancestor, "PropertyDefinition") || isNodeOfType(ancestor, "AccessorProperty")) {
28514
+ if (!ancestor.static) return false;
28515
+ ancestor = ancestor.parent ?? null;
28516
+ continue;
28517
+ }
28518
+ if (isNodeOfType(ancestor, "StaticBlock")) {
28519
+ ancestor = ancestor.parent ?? null;
28520
+ continue;
28521
+ }
28522
+ if (isFunctionLike$1(ancestor)) {
28523
+ const functionRoot = findTransparentExpressionRoot(ancestor);
28524
+ const invocation = functionRoot.parent;
28525
+ if (isNodeOfType(invocation, "CallExpression") && stripParenExpression(invocation.callee) === functionRoot) {
28526
+ ancestor = invocation.parent ?? null;
28527
+ continue;
28528
+ }
28529
+ return false;
28530
+ }
28531
+ ancestor = ancestor.parent ?? null;
28532
+ }
28533
+ return true;
28534
+ };
28535
+ const getFunctionName = (functionNode) => {
28536
+ if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id.name;
28537
+ const parent = functionNode.parent;
28538
+ return isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier") ? parent.id.name : null;
28539
+ };
28540
+ const isModuleScopedFunction = (functionNode) => {
28541
+ let ancestor = functionNode.parent;
28542
+ while (ancestor) {
28543
+ if (isFunctionLike$1(ancestor) || isNodeOfType(ancestor, "ClassBody")) return false;
28544
+ if (isNodeOfType(ancestor, "Program")) return true;
28545
+ ancestor = ancestor.parent ?? null;
28546
+ }
28547
+ return false;
28548
+ };
28549
+ const getDirectCalls = (functionNode, scopes) => {
28550
+ const bindingIdentifier = isNodeOfType(functionNode, "FunctionDeclaration") ? functionNode.id : isNodeOfType(functionNode.parent, "VariableDeclarator") ? functionNode.parent.id : null;
28551
+ if (!bindingIdentifier || !isNodeOfType(bindingIdentifier, "Identifier")) return [];
28552
+ const symbol = scopes.scopeFor(functionNode).symbolsByName.get(bindingIdentifier.name);
28553
+ if (!symbol) return [];
28554
+ const calls = [];
28555
+ const program = findProgramRoot(functionNode);
28556
+ if (!program) return calls;
28557
+ walkAst(program, (candidate) => {
28558
+ if (!isNodeOfType(candidate, "CallExpression")) return;
28559
+ const callee = stripParenExpression(candidate.callee);
28560
+ if (!isNodeOfType(callee, "Identifier")) return;
28561
+ if (resolveConstIdentifierAlias(callee, scopes)?.id === symbol.id) calls.push(candidate);
28562
+ });
28563
+ return calls;
28564
+ };
28565
+ const processLifetimeClassSymbolIdsByAnalysis = /* @__PURE__ */ new WeakMap();
28566
+ const getProcessLifetimeClassSymbolIds = (node, scopes) => {
28567
+ const cached = processLifetimeClassSymbolIdsByAnalysis.get(scopes);
28568
+ if (cached) return cached;
28569
+ const instantiationScopeBySymbolId = /* @__PURE__ */ new Map();
28570
+ const program = findProgramRoot(node);
28571
+ if (program) walkAst(program, (candidate) => {
28572
+ if (!isNodeOfType(candidate, "NewExpression")) return;
28573
+ const callee = stripParenExpression(candidate.callee);
28574
+ const symbol = isNodeOfType(callee, "Identifier") ? resolveConstIdentifierAlias(callee, scopes) : null;
28575
+ if (!symbol) return;
28576
+ const wasModuleOnly = instantiationScopeBySymbolId.get(symbol.id) ?? true;
28577
+ instantiationScopeBySymbolId.set(symbol.id, wasModuleOnly && isEvaluatedAtModuleScope(candidate));
28578
+ });
28579
+ const processLifetimeSymbolIds = /* @__PURE__ */ new Set();
28580
+ for (const [symbolId, isModuleOnly] of instantiationScopeBySymbolId) if (isModuleOnly) processLifetimeSymbolIds.add(symbolId);
28581
+ processLifetimeClassSymbolIdsByAnalysis.set(scopes, processLifetimeSymbolIds);
28582
+ return processLifetimeSymbolIds;
28583
+ };
28584
+ const isProcessLifetimeWiring = (node, scopes) => {
28585
+ let ancestor = node.parent;
28586
+ while (ancestor) {
28587
+ if (!isFunctionLike$1(ancestor)) {
28588
+ ancestor = ancestor.parent ?? null;
28589
+ continue;
28590
+ }
28591
+ const functionName = getFunctionName(ancestor);
28592
+ if (functionName && PROCESS_LIFETIME_WIRING_NAME_PATTERN.test(functionName) && isModuleScopedFunction(ancestor)) {
28593
+ const calls = getDirectCalls(ancestor, scopes);
28594
+ return calls.length > 0 && calls.every(isEvaluatedAtModuleScope);
28595
+ }
28596
+ const methodDefinition = ancestor.parent;
28597
+ if (isNodeOfType(methodDefinition, "MethodDefinition") && methodDefinition.kind === "constructor") {
28598
+ let classNode = methodDefinition.parent?.parent;
28599
+ if (isNodeOfType(classNode, "ClassDeclaration") || isNodeOfType(classNode, "ClassExpression")) {
28600
+ const classRoot = findTransparentExpressionRoot(classNode);
28601
+ const classInstantiation = classRoot.parent;
28602
+ if (isNodeOfType(classNode, "ClassExpression") && isNodeOfType(classInstantiation, "NewExpression") && stripParenExpression(classInstantiation.callee) === classRoot) return isEvaluatedAtModuleScope(classInstantiation);
28603
+ const classSymbol = getClassBindingSymbol(classNode, scopes);
28604
+ return Boolean(classSymbol && getProcessLifetimeClassSymbolIds(node, scopes).has(classSymbol.id));
28605
+ }
28606
+ }
28607
+ return false;
28608
+ }
28609
+ return false;
28610
+ };
28611
+ const mayCarryAbortSignal = (optionsArgument, scopes) => {
28612
+ if (!optionsArgument) return false;
28613
+ const options = resolveStableOptionsObject(optionsArgument, ["signal"], scopes);
28614
+ if (!options) return true;
28615
+ return options.properties.some((property) => {
28616
+ if (!isNodeOfType(property, "Property")) return true;
28617
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
28618
+ if (propertyName === null) return true;
28619
+ if (propertyName !== "signal") return false;
28620
+ const value = property.value;
28621
+ if (isNodeOfType(value, "Identifier") && value.name === "undefined") return false;
28622
+ if (isNodeOfType(value, "Literal") && value.value == null) return false;
28623
+ return !(isNodeOfType(value, "UnaryExpression") && value.operator === "void");
28624
+ });
28625
+ };
28626
+ const callbackDisposesReaction = (callExpression, subscriptionName, scopes) => {
28627
+ const callbackArgument = callExpression.arguments[REACTION_CALLBACK_INDEX[subscriptionName]];
28628
+ if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return false;
28629
+ const callback = resolveExactLocalFunction(callbackArgument, scopes);
28630
+ if (!callback || !isFunctionLike$1(callback)) return false;
28631
+ const reactionParameter = callback.params?.[REACTION_PARAMETER_INDEX[subscriptionName]];
28632
+ if (!reactionParameter || !isNodeOfType(reactionParameter, "Identifier")) return false;
28633
+ const reactionSymbol = scopes.symbolFor(reactionParameter);
28634
+ if (!reactionSymbol) return false;
28635
+ let doesDisposeReaction = false;
28636
+ walkAst(callback, (candidate) => {
28637
+ if (doesDisposeReaction || !isNodeOfType(candidate, "CallExpression")) return;
28638
+ const callee = stripParenExpression(candidate.callee);
28639
+ if (!isNodeOfType(callee, "MemberExpression")) return;
28640
+ const receiver = stripParenExpression(callee.object);
28641
+ if (isNodeOfType(receiver, "Identifier") && scopes.symbolFor(receiver)?.id === reactionSymbol.id && getStaticPropertyKeyName(callee, { allowComputedString: true }) === "dispose") doesDisposeReaction = true;
28642
+ });
28643
+ return doesDisposeReaction;
28644
+ };
28645
+ const getMemberReceiverRoot = (memberExpression) => {
28646
+ let receiver = stripParenExpression(memberExpression);
28647
+ while (isNodeOfType(receiver, "MemberExpression")) receiver = stripParenExpression(receiver.object);
28648
+ return receiver;
28649
+ };
28650
+ const observesOnlyInstanceRootedState = (callExpression, subscriptionName, scopes) => {
28651
+ const callbackArgument = callExpression.arguments[OBSERVATION_CALLBACK_INDEX[subscriptionName]];
28652
+ if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return false;
28653
+ const callback = resolveExactLocalFunction(callbackArgument, scopes);
28654
+ if (!callback || !isFunctionLike$1(callback)) return false;
28655
+ let observesExternalState = false;
28656
+ walkAst(callback, (candidate) => {
28657
+ if (candidate !== callback && isFunctionLike$1(candidate)) return false;
28658
+ if (observesExternalState || !isNodeOfType(candidate, "MemberExpression")) return;
28659
+ const receiver = getMemberReceiverRoot(candidate);
28660
+ const candidateRoot = findTransparentExpressionRoot(candidate);
28661
+ const parent = candidateRoot.parent;
28662
+ const isDirectMethodCall = isNodeOfType(parent, "CallExpression") && parent.callee === candidateRoot;
28663
+ if (isNodeOfType(receiver, "ThisExpression")) {
28664
+ if (isDirectMethodCall) observesExternalState = true;
28665
+ return;
28666
+ }
28667
+ if (!isNodeOfType(receiver, "Identifier")) {
28668
+ observesExternalState = true;
28669
+ return;
28670
+ }
28671
+ const receiverSymbol = scopes.symbolFor(receiver);
28672
+ if (receiverSymbol && isAstDescendant(receiverSymbol.declarationNode, callback) || scopes.isGlobalReference(receiver) && NON_OBSERVABLE_GLOBAL_RECEIVER_NAMES.has(receiver.name)) return;
28673
+ if (receiverSymbol?.kind === "import" && isDirectMethodCall && NON_OBSERVING_IMPORTED_METHOD_NAMES.has(getStaticPropertyKeyName(candidate, { allowComputedString: true }) ?? "")) return;
28674
+ observesExternalState = true;
28675
+ });
28676
+ return !observesExternalState;
28677
+ };
28678
+ const isForwardedFromConciseArrow = (callExpression) => {
28679
+ let expressionRoot = findTransparentExpressionRoot(callExpression);
28680
+ let parent = expressionRoot.parent;
28681
+ while (parent) {
28682
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === expressionRoot) return true;
28683
+ if (isNodeOfType(parent, "LogicalExpression") && (parent.right === expressionRoot || parent.left === expressionRoot && parent.operator !== "&&") || isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === expressionRoot || parent.alternate === expressionRoot) || isNodeOfType(parent, "SequenceExpression") && parent.expressions[parent.expressions.length - 1] === expressionRoot) {
28684
+ expressionRoot = findTransparentExpressionRoot(parent);
28685
+ parent = expressionRoot.parent;
28686
+ continue;
28687
+ }
28688
+ return false;
28689
+ }
28690
+ return false;
28691
+ };
28692
+ const isDisposerDiscarded = (callExpression) => {
28693
+ if (isForwardedFromConciseArrow(callExpression)) return false;
28694
+ const expressionRoot = findTransparentExpressionRoot(callExpression);
28695
+ if (isResultDiscardedCall(callExpression)) return true;
28696
+ const parent = expressionRoot.parent;
28697
+ if (!parent) return false;
28698
+ if (isNodeOfType(parent, "UnaryExpression") || isNodeOfType(parent, "BinaryExpression")) return true;
28699
+ if ((isNodeOfType(parent, "IfStatement") || isNodeOfType(parent, "WhileStatement") || isNodeOfType(parent, "DoWhileStatement") || isNodeOfType(parent, "ForStatement")) && parent.test === expressionRoot) return true;
28700
+ if (isNodeOfType(parent, "ConditionalExpression") && parent.test === expressionRoot || isNodeOfType(parent, "SwitchStatement") && parent.discriminant === expressionRoot) return true;
28701
+ if (isNodeOfType(parent, "LogicalExpression") && parent.left === expressionRoot) return parent.operator === "&&" || isResultDiscardedCall(parent);
28702
+ const callee = isNodeOfType(parent, "CallExpression") ? stripParenExpression(parent.callee) : null;
28703
+ return Boolean(isNodeOfType(parent, "CallExpression") && parent.arguments.some((argument) => argument === expressionRoot) && isNodeOfType(callee, "Identifier") && DISPOSER_COERCION_NAMES.has(callee.name));
28704
+ };
28705
+ const mobxReactionDisposerDiscarded = defineRule({
28706
+ id: "mobx-reaction-disposer-discarded",
28707
+ title: "MobX reaction disposer discarded",
28708
+ severity: "warn",
28709
+ category: "Bugs",
28710
+ requires: MOBX_RULE_GATES["mobx-reaction-disposer-discarded"].requires,
28711
+ recommendation: "Keep the disposer returned by `reaction` or `autorun` and invoke it during teardown, or provide an AbortSignal.",
28712
+ create: (context) => ({ CallExpression(callExpression) {
28713
+ const subscriptionName = resolveLeakingSubscriptionName(callExpression, context.scopes);
28714
+ if (!subscriptionName || !isDisposerDiscarded(callExpression)) return;
28715
+ if (isEvaluatedAtModuleScope(callExpression)) return;
28716
+ if (isProcessLifetimeWiring(callExpression, context.scopes)) return;
28717
+ if (callbackDisposesReaction(callExpression, subscriptionName, context.scopes)) return;
28718
+ if (observesOnlyInstanceRootedState(callExpression, subscriptionName, context.scopes)) return;
28719
+ const optionsArgument = callExpression.arguments[OPTIONS_ARGUMENT_INDEX[subscriptionName]];
28720
+ if (mayCarryAbortSignal(optionsArgument, context.scopes)) return;
28721
+ context.report({
28722
+ node: callExpression,
28723
+ message: MESSAGE$41
28724
+ });
28725
+ } })
28726
+ });
28727
+ //#endregion
28189
28728
  //#region src/plugin/utils/has-jsx-prop.ts
28190
28729
  const hasJsxProp = (attributes, targetProp) => {
28191
28730
  for (const attribute of attributes) {
@@ -28976,11 +29515,11 @@ const hasEmailTemplateImport = (programRoot) => {
28976
29515
  return found;
28977
29516
  };
28978
29517
  //#endregion
28979
- //#region src/plugin/utils/build-generated-image-project-index.ts
28980
- const GENERATED_IMAGE_SOURCE_FILE_PATTERN = /\.[cm]?[jt]sx?$/i;
28981
- const GENERATED_IMAGE_DECLARATION_FILE_PATTERN = /\.d\.[cm]?[jt]s$/i;
28982
- const GENERATED_IMAGE_MDX_FILE_PATTERN = /\.mdx$/i;
28983
- const GENERATED_IMAGE_IGNORED_DIRECTORY_NAMES = new Set([
29518
+ //#region src/plugin/utils/build-source-project-index.ts
29519
+ const SOURCE_PROJECT_FILE_PATTERN = /\.[cm]?[jt]sx?$/i;
29520
+ const SOURCE_PROJECT_DECLARATION_FILE_PATTERN = /\.d\.[cm]?[jt]s$/i;
29521
+ const SOURCE_PROJECT_MDX_FILE_PATTERN = /\.mdx$/i;
29522
+ const SOURCE_PROJECT_IGNORED_DIRECTORY_NAMES = new Set([
28984
29523
  ".angular",
28985
29524
  ".astro",
28986
29525
  ".cache",
@@ -29001,12 +29540,12 @@ const GENERATED_IMAGE_IGNORED_DIRECTORY_NAMES = new Set([
29001
29540
  "out",
29002
29541
  "storybook-static"
29003
29542
  ]);
29004
- const generatedImageScopeCache = /* @__PURE__ */ new WeakMap();
29005
- const getGeneratedImageModuleScopes = (programNode) => {
29006
- const cachedScopes = generatedImageScopeCache.get(programNode);
29543
+ const sourceProjectScopeCache = /* @__PURE__ */ new WeakMap();
29544
+ const getSourceProjectModuleScopes = (programNode) => {
29545
+ const cachedScopes = sourceProjectScopeCache.get(programNode);
29007
29546
  if (cachedScopes) return cachedScopes;
29008
29547
  const scopes = analyzeScopes(programNode);
29009
- generatedImageScopeCache.set(programNode, scopes);
29548
+ sourceProjectScopeCache.set(programNode, scopes);
29010
29549
  return scopes;
29011
29550
  };
29012
29551
  const listProductionSourceFiles = (rootDirectory) => {
@@ -29024,7 +29563,7 @@ const listProductionSourceFiles = (rootDirectory) => {
29024
29563
  }
29025
29564
  for (const entry of entries) {
29026
29565
  const absolutePath = path.join(currentDirectory, entry.name);
29027
- const isIgnoredDirectoryName = GENERATED_IMAGE_IGNORED_DIRECTORY_NAMES.has(entry.name) || entry.name.startsWith(".") && entry.name !== ".dumi" && entry.name !== ".storybook";
29566
+ const isIgnoredDirectoryName = SOURCE_PROJECT_IGNORED_DIRECTORY_NAMES.has(entry.name) || entry.name.startsWith(".") && entry.name !== ".dumi" && entry.name !== ".storybook";
29028
29567
  if (entry.isSymbolicLink() && isIgnoredDirectoryName) continue;
29029
29568
  if (entry.isSymbolicLink()) return null;
29030
29569
  if (entry.isDirectory()) {
@@ -29033,12 +29572,12 @@ const listProductionSourceFiles = (rootDirectory) => {
29033
29572
  continue;
29034
29573
  }
29035
29574
  if (!entry.isFile() || isTestlikeFilename(absolutePath)) continue;
29036
- if (GENERATED_IMAGE_MDX_FILE_PATTERN.test(entry.name)) {
29575
+ if (SOURCE_PROJECT_MDX_FILE_PATTERN.test(entry.name)) {
29037
29576
  hasOpaqueMdxConsumerSurface = true;
29038
29577
  continue;
29039
29578
  }
29040
- if (!GENERATED_IMAGE_SOURCE_FILE_PATTERN.test(entry.name)) continue;
29041
- if (GENERATED_IMAGE_DECLARATION_FILE_PATTERN.test(entry.name)) continue;
29579
+ if (!SOURCE_PROJECT_FILE_PATTERN.test(entry.name)) continue;
29580
+ if (SOURCE_PROJECT_DECLARATION_FILE_PATTERN.test(entry.name)) continue;
29042
29581
  sourceFilePaths.push(normalizeFilename(absolutePath));
29043
29582
  }
29044
29583
  }
@@ -29083,7 +29622,7 @@ const indexModuleSources = (module, consumerModulesByFilePath, resolvedSourcePat
29083
29622
  consumerModulesByFilePath.set(normalizedSourcePath, consumerModules);
29084
29623
  });
29085
29624
  };
29086
- const buildGeneratedImageProjectIndex = (rootDirectory, currentFilePath, currentProgramNode, currentScopes) => {
29625
+ const buildSourceProjectIndex = (rootDirectory, currentFilePath, currentProgramNode, currentScopes) => {
29087
29626
  const productionSourceFiles = listProductionSourceFiles(rootDirectory);
29088
29627
  if (!productionSourceFiles) return null;
29089
29628
  const modulesByFilePath = /* @__PURE__ */ new Map();
@@ -29101,7 +29640,7 @@ const buildGeneratedImageProjectIndex = (rootDirectory, currentFilePath, current
29101
29640
  modulesByFilePath.set(filePath, {
29102
29641
  filePath,
29103
29642
  programNode: parsedProgram,
29104
- scopes: getGeneratedImageModuleScopes(parsedProgram)
29643
+ scopes: getSourceProjectModuleScopes(parsedProgram)
29105
29644
  });
29106
29645
  }
29107
29646
  const consumerModulesByFilePath = /* @__PURE__ */ new Map();
@@ -29117,6 +29656,51 @@ const buildGeneratedImageProjectIndex = (rootDirectory, currentFilePath, current
29117
29656
  };
29118
29657
  };
29119
29658
  //#endregion
29659
+ //#region src/plugin/utils/get-direct-function-binding-identifier.ts
29660
+ const getDirectFunctionBindingIdentifier = (functionNode) => {
29661
+ if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
29662
+ const functionValueRoot = findTransparentExpressionRoot(functionNode);
29663
+ const parent = functionValueRoot.parent;
29664
+ return isNodeOfType(parent, "VariableDeclarator") && parent.init === functionValueRoot && isNodeOfType(parent.id, "Identifier") ? parent.id : null;
29665
+ };
29666
+ //#endregion
29667
+ //#region src/plugin/utils/get-function-export-names.ts
29668
+ const getExportedSpecifierName$1 = (specifier) => {
29669
+ const exported = specifier.exported;
29670
+ if (isNodeOfType(exported, "Identifier")) return exported.name;
29671
+ return isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null;
29672
+ };
29673
+ const getLocalSpecifierName = (specifier) => {
29674
+ const local = specifier.local;
29675
+ if (isNodeOfType(local, "Identifier")) return local.name;
29676
+ return isNodeOfType(local, "Literal") && typeof local.value === "string" ? local.value : null;
29677
+ };
29678
+ const getFunctionExportNames = (programNode, functionNode) => {
29679
+ const functionValueRoot = findTransparentExpressionRoot(functionNode);
29680
+ const bindingName = getDirectFunctionBindingIdentifier(functionNode)?.name ?? null;
29681
+ const exportedNames = /* @__PURE__ */ new Set();
29682
+ for (const statement of programNode.body) {
29683
+ if (isNodeOfType(statement, "ExportDefaultDeclaration")) {
29684
+ if (statement.declaration === functionValueRoot || bindingName && isNodeOfType(statement.declaration, "Identifier") && statement.declaration.name === bindingName) exportedNames.add("default");
29685
+ continue;
29686
+ }
29687
+ if (!isNodeOfType(statement, "ExportNamedDeclaration")) continue;
29688
+ const declaration = statement.declaration;
29689
+ if (declaration === functionValueRoot && bindingName) exportedNames.add(bindingName);
29690
+ if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
29691
+ for (const declarator of declaration.declarations) if (declarator.init === functionValueRoot && isNodeOfType(declarator.id, "Identifier")) exportedNames.add(declarator.id.name);
29692
+ }
29693
+ if (!bindingName || statement.source) continue;
29694
+ for (const specifier of statement.specifiers) {
29695
+ if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
29696
+ if (getLocalSpecifierName(specifier) !== bindingName) continue;
29697
+ const exportedName = getExportedSpecifierName$1(specifier);
29698
+ if (exportedName) exportedNames.add(exportedName);
29699
+ }
29700
+ }
29701
+ return [...exportedNames];
29702
+ };
29703
+ //#endregion
29120
29704
  //#region src/plugin/utils/read-nearest-package-manifest.ts
29121
29705
  const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
29122
29706
  const cachedManifestByPackageDirectory = /* @__PURE__ */ new Map();
@@ -29201,37 +29785,6 @@ const getImportSpecifierName = (specifier) => {
29201
29785
  if (isNodeOfType(imported, "Identifier")) return imported.name;
29202
29786
  return isNodeOfType(imported, "Literal") && typeof imported.value === "string" ? imported.value : null;
29203
29787
  };
29204
- const getDirectFunctionBindingIdentifier = (functionNode) => {
29205
- if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
29206
- const functionValueRoot = findTransparentExpressionRoot(functionNode);
29207
- const parent = functionValueRoot.parent;
29208
- return isNodeOfType(parent, "VariableDeclarator") && parent.init === functionValueRoot && isNodeOfType(parent.id, "Identifier") ? parent.id : null;
29209
- };
29210
- const getExportNamesForFunction = (programNode, functionNode) => {
29211
- const functionValueRoot = findTransparentExpressionRoot(functionNode);
29212
- const bindingName = getDirectFunctionBindingIdentifier(functionNode)?.name ?? null;
29213
- const exportedNames = /* @__PURE__ */ new Set();
29214
- for (const statement of programNode.body) {
29215
- if (isNodeOfType(statement, "ExportDefaultDeclaration")) {
29216
- if (statement.declaration === functionValueRoot || bindingName && isNodeOfType(statement.declaration, "Identifier") && statement.declaration.name === bindingName) exportedNames.add("default");
29217
- continue;
29218
- }
29219
- if (!isNodeOfType(statement, "ExportNamedDeclaration")) continue;
29220
- const declaration = statement.declaration;
29221
- if (declaration === functionValueRoot && bindingName) exportedNames.add(bindingName);
29222
- if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
29223
- for (const declarator of declaration.declarations) if (declarator.init === functionValueRoot && isNodeOfType(declarator.id, "Identifier")) exportedNames.add(declarator.id.name);
29224
- }
29225
- if (!bindingName || statement.source) continue;
29226
- for (const specifier of statement.specifiers) {
29227
- if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
29228
- if (getImportedSpecifierName(specifier) !== bindingName) continue;
29229
- const exportedName = getExportedSpecifierName(specifier);
29230
- if (exportedName) exportedNames.add(exportedName);
29231
- }
29232
- }
29233
- return [...exportedNames];
29234
- };
29235
29788
  const isTransparentGeneratedImageValueFlow = (expression, target) => {
29236
29789
  let current = findTransparentExpressionRoot(expression);
29237
29790
  while (current !== target) {
@@ -29290,7 +29843,7 @@ const classifyInvokedExpression = (module, expression, state) => {
29290
29843
  }
29291
29844
  const forwardingFunction = getForwardingFunction(expression);
29292
29845
  if (!forwardingFunction) return false;
29293
- const exportedNames = getExportNamesForFunction(module.programNode, forwardingFunction);
29846
+ const exportedNames = getFunctionExportNames(module.programNode, forwardingFunction);
29294
29847
  if (exportedNames.length === 0) return false;
29295
29848
  for (const exportedName of exportedNames) enqueueExport(state, module.filePath, exportedName);
29296
29849
  return true;
@@ -29423,9 +29976,9 @@ const createExportedJsxGeneratedImageOwnershipAnalyzer = (context) => {
29423
29976
  const programNode = findProgramRoot(jsxNode);
29424
29977
  const enclosingFunction = findEnclosingFunction$1(jsxNode);
29425
29978
  if (!programNode || !enclosingFunction) return false;
29426
- const initialExportNames = getExportNamesForFunction(programNode, enclosingFunction);
29979
+ const initialExportNames = getFunctionExportNames(programNode, enclosingFunction);
29427
29980
  if (initialExportNames.length === 0) return false;
29428
- if (projectIndex === void 0) projectIndex = buildGeneratedImageProjectIndex(rootDirectory, filename, programNode, context.scopes);
29981
+ if (projectIndex === void 0) projectIndex = buildSourceProjectIndex(rootDirectory, filename, programNode, context.scopes);
29429
29982
  if (!projectIndex || projectIndex.hasOpaqueMdxConsumerSurface) return false;
29430
29983
  const state = {
29431
29984
  projectIndex,
@@ -41733,18 +42286,10 @@ const isCompletionSinkAfterCancellationEarlyExit = (completionSink, cancellation
41733
42286
  return false;
41734
42287
  };
41735
42288
  const isCompletionSinkGuardedByCancellationFlag = (completionSink, cancellationFlagKey, context) => isCompletionSinkInsideCancellationGuard(completionSink, cancellationFlagKey, context) || isCompletionSinkAfterCancellationEarlyExit(completionSink, cancellationFlagKey, context);
41736
- const isDescendantOf = (node, ancestor) => {
41737
- let currentNode = node;
41738
- while (currentNode) {
41739
- if (currentNode === ancestor) return true;
41740
- currentNode = currentNode.parent ?? null;
41741
- }
41742
- return false;
41743
- };
41744
42289
  const isPromiseContinuationForRequest = (functionNode, request) => {
41745
42290
  const callNode = functionNode.parent;
41746
42291
  if (!isNodeOfType(callNode, "CallExpression") || !callNode.arguments?.some((callArgument) => callArgument === functionNode) || !isNodeOfType(callNode.callee, "MemberExpression") || callNode.callee.computed || !isNodeOfType(callNode.callee.property, "Identifier") || !PROMISE_CONTINUATION_METHOD_NAMES$1.has(callNode.callee.property.name)) return false;
41747
- return isDescendantOf(request, callNode.callee.object);
42292
+ return isAstDescendant(request, callNode.callee.object);
41748
42293
  };
41749
42294
  const isAwaitedInFunction = (request, functionNode) => {
41750
42295
  let currentNode = request.parent;
@@ -45416,41 +45961,6 @@ const noMutableInDeps = defineRule({
45416
45961
  }
45417
45962
  });
45418
45963
  //#endregion
45419
- //#region src/plugin/utils/is-result-discarded-call.ts
45420
- const isResultDiscardedCall = (callExpression) => {
45421
- let node = callExpression;
45422
- let parent = node.parent;
45423
- while (parent) {
45424
- if (isNodeOfType(parent, "ExpressionStatement")) return true;
45425
- if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
45426
- if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
45427
- if (isNodeOfType(parent, "ChainExpression")) {
45428
- node = parent;
45429
- parent = node.parent;
45430
- continue;
45431
- }
45432
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
45433
- node = parent;
45434
- parent = node.parent;
45435
- continue;
45436
- }
45437
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
45438
- node = parent;
45439
- parent = node.parent;
45440
- continue;
45441
- }
45442
- if (isNodeOfType(parent, "SequenceExpression")) {
45443
- const expressions = parent.expressions ?? [];
45444
- if (expressions[expressions.length - 1] !== node) return true;
45445
- node = parent;
45446
- parent = node.parent;
45447
- continue;
45448
- }
45449
- return false;
45450
- }
45451
- return false;
45452
- };
45453
- //#endregion
45454
45964
  //#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
45455
45965
  const LODASH_MUTATOR_NAMES = new Set([
45456
45966
  "set",
@@ -53688,7 +54198,7 @@ const isElementTypeJsxAttribute = (node) => {
53688
54198
  const attributeName = node.name.name;
53689
54199
  return ELEMENT_TYPE_PROP_NAMES.has(attributeName.toLowerCase()) || attributeName.endsWith("Component");
53690
54200
  };
53691
- const isReactUseMemoCallback = (call, valueNode, scopes) => call.arguments[0] === valueNode && isReactApiCall(call, "useMemo", scopes, {
54201
+ const isReactUseMemoCallback$1 = (call, valueNode, scopes) => call.arguments[0] === valueNode && isReactApiCall(call, "useMemo", scopes, {
53692
54202
  allowGlobalReactNamespace: true,
53693
54203
  resolveNamedAliases: true
53694
54204
  });
@@ -53712,7 +54222,7 @@ const isRenderFlowingReadReference = (identifier, scopes, visitedSymbols = /* @_
53712
54222
  case "ArrowFunctionExpression": return false;
53713
54223
  case "CallExpression":
53714
54224
  if (parent.callee === valueNode) return false;
53715
- if (isReactUseMemoCallback(parent, valueNode, scopes)) return false;
54225
+ if (isReactUseMemoCallback$1(parent, valueNode, scopes)) return false;
53716
54226
  if (isReactCreateElementCall(parent, scopes)) return true;
53717
54227
  valueNode = parent;
53718
54228
  parent = parent.parent;
@@ -58739,6 +59249,560 @@ const reduxUseselectorReturnsNewCollection = defineRule({
58739
59249
  }
58740
59250
  });
58741
59251
  //#endregion
59252
+ //#region src/plugin/utils/is-remotion-module-source.ts
59253
+ const isRemotionModuleSource = (moduleSource) => moduleSource === "remotion" || moduleSource.startsWith("@remotion/");
59254
+ //#endregion
59255
+ //#region src/plugin/utils/resolve-remotion-api.ts
59256
+ const resolveRemotionApi = (referenceNode, scopes) => {
59257
+ const candidate = stripParenExpression(referenceNode);
59258
+ if (isNodeOfType(candidate, "Identifier") || isNodeOfType(candidate, "JSXIdentifier")) {
59259
+ const symbol = scopes.symbolFor(candidate);
59260
+ if (symbol?.kind !== "import") return null;
59261
+ const importBinding = getImportBindingForName(candidate, symbol.name);
59262
+ if (!importBinding || importBinding.isNamespace || importBinding.exportedName === null || !isRemotionModuleSource(importBinding.source)) return null;
59263
+ return {
59264
+ apiName: importBinding.exportedName,
59265
+ moduleSource: importBinding.source
59266
+ };
59267
+ }
59268
+ if (isNodeOfType(candidate, "MemberExpression")) {
59269
+ const apiName = getStaticPropertyKeyName(candidate, { allowComputedString: true });
59270
+ const namespaceObject = stripParenExpression(candidate.object);
59271
+ if (!apiName || !isNodeOfType(namespaceObject, "Identifier")) return null;
59272
+ const symbol = scopes.symbolFor(namespaceObject);
59273
+ if (symbol?.kind !== "import") return null;
59274
+ const importBinding = getImportBindingForName(namespaceObject, symbol.name);
59275
+ if (!importBinding?.isNamespace || !isRemotionModuleSource(importBinding.source)) return null;
59276
+ return {
59277
+ apiName,
59278
+ moduleSource: importBinding.source
59279
+ };
59280
+ }
59281
+ if (!isNodeOfType(candidate, "JSXMemberExpression") || !isNodeOfType(candidate.object, "JSXIdentifier") || !isNodeOfType(candidate.property, "JSXIdentifier")) return null;
59282
+ const symbol = scopes.symbolFor(candidate.object);
59283
+ if (symbol?.kind !== "import") return null;
59284
+ const importBinding = getImportBindingForName(candidate.object, symbol.name);
59285
+ if (!importBinding?.isNamespace || !isRemotionModuleSource(importBinding.source)) return null;
59286
+ return {
59287
+ apiName: candidate.property.name,
59288
+ moduleSource: importBinding.source
59289
+ };
59290
+ };
59291
+ //#endregion
59292
+ //#region src/plugin/utils/create-remotion-composition-ownership-analyzer.ts
59293
+ const compositionAttributeFunctionCacheBySettings = /* @__PURE__ */ new WeakMap();
59294
+ const getFunctionExportKeys = (filePath, programNode, functionNode) => getFunctionExportNames(programNode, functionNode).map((exportedName) => `${normalizeFilename(filePath)}\0${exportedName}`);
59295
+ const resolveImportedCompositionFunction = (expression, module) => {
59296
+ const unwrappedExpression = stripParenExpression(expression);
59297
+ let importReference;
59298
+ let exportedName;
59299
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
59300
+ const symbol = module.scopes.symbolFor(unwrappedExpression);
59301
+ if (symbol?.kind !== "import") return null;
59302
+ importReference = unwrappedExpression;
59303
+ exportedName = getImportBindingForName(unwrappedExpression, symbol.name)?.exportedName ?? null;
59304
+ } else if (isNodeOfType(unwrappedExpression, "MemberExpression") && isNodeOfType(stripParenExpression(unwrappedExpression.object), "Identifier")) {
59305
+ const namespaceObject = stripParenExpression(unwrappedExpression.object);
59306
+ if (!isNodeOfType(namespaceObject, "Identifier")) return null;
59307
+ const symbol = module.scopes.symbolFor(namespaceObject);
59308
+ if (symbol?.kind !== "import") return null;
59309
+ if (!getImportBindingForName(namespaceObject, symbol.name)?.isNamespace) return null;
59310
+ importReference = namespaceObject;
59311
+ exportedName = getStaticPropertyKeyName(unwrappedExpression, { allowComputedString: true });
59312
+ } else return null;
59313
+ if (!exportedName) return null;
59314
+ const importBinding = getImportBindingForName(importReference, importReference.name);
59315
+ if (!importBinding) return null;
59316
+ return resolveCrossFileFunctionExportWithFilePath(module.filePath, importBinding.source, exportedName);
59317
+ };
59318
+ const collectCompositionAttributeFunctionKeys = (context, currentProgram, attributeName) => {
59319
+ const filename = context.filename ? normalizeFilename(context.filename) : "";
59320
+ const rootDirectorySetting = getReactDoctorStringSetting(context.settings, "rootDirectory");
59321
+ const rootDirectory = rootDirectorySetting ? normalizeFilename(rootDirectorySetting).replace(/\/$/, "") : "";
59322
+ if (!filename || !rootDirectory || filename !== rootDirectory && !filename.startsWith(`${rootDirectory}/`)) return null;
59323
+ const projectIndex = buildSourceProjectIndex(rootDirectory, filename, currentProgram, context.scopes);
59324
+ if (!projectIndex) return null;
59325
+ const functionKeys = /* @__PURE__ */ new Set();
59326
+ for (const module of projectIndex.modulesByFilePath.values()) walkAst(module.programNode, (candidate) => {
59327
+ if (!isNodeOfType(candidate, "JSXOpeningElement")) return;
59328
+ const apiBinding = resolveRemotionApi(candidate.name, module.scopes);
59329
+ if (apiBinding?.apiName !== "Composition" || apiBinding.moduleSource !== "remotion") return;
59330
+ const functionAttribute = findJsxAttribute(candidate.attributes, attributeName);
59331
+ if (!functionAttribute?.value || !isNodeOfType(functionAttribute.value, "JSXExpressionContainer") || !functionAttribute.value.expression) return;
59332
+ const resolvedFunction = resolveImportedCompositionFunction(functionAttribute.value.expression, module);
59333
+ if (!resolvedFunction || !isNodeOfType(resolvedFunction.programNode, "Program")) return;
59334
+ for (const functionKey of getFunctionExportKeys(resolvedFunction.filePath, resolvedFunction.programNode, resolvedFunction.functionNode)) functionKeys.add(functionKey);
59335
+ });
59336
+ return functionKeys;
59337
+ };
59338
+ const createRemotionCompositionAttributeOwnershipAnalyzer = (context, attributeName) => {
59339
+ const settings = context.settings;
59340
+ return (functionNode) => {
59341
+ const currentProgram = findProgramRoot(functionNode);
59342
+ if (!currentProgram || !isNodeOfType(currentProgram, "Program") || !context.filename || !settings) return false;
59343
+ const currentFunctionKeys = getFunctionExportKeys(context.filename, currentProgram, functionNode);
59344
+ if (currentFunctionKeys.length === 0) return false;
59345
+ let cacheByAttributeName = compositionAttributeFunctionCacheBySettings.get(settings);
59346
+ if (!cacheByAttributeName) {
59347
+ cacheByAttributeName = /* @__PURE__ */ new Map();
59348
+ compositionAttributeFunctionCacheBySettings.set(settings, cacheByAttributeName);
59349
+ }
59350
+ let cache = cacheByAttributeName.get(attributeName);
59351
+ if (!cache) {
59352
+ cache = { failedFilenames: /* @__PURE__ */ new Set() };
59353
+ cacheByAttributeName.set(attributeName, cache);
59354
+ }
59355
+ let functionKeys = cache.functionKeys;
59356
+ if (!functionKeys) {
59357
+ const filename = normalizeFilename(context.filename);
59358
+ if (cache.failedFilenames.has(filename)) return false;
59359
+ const collectedFunctionKeys = collectCompositionAttributeFunctionKeys(context, currentProgram, attributeName);
59360
+ if (!collectedFunctionKeys) {
59361
+ cache.failedFilenames.add(filename);
59362
+ return false;
59363
+ }
59364
+ functionKeys = collectedFunctionKeys;
59365
+ cache.functionKeys = collectedFunctionKeys;
59366
+ }
59367
+ return currentFunctionKeys.some((functionKey) => functionKeys.has(functionKey));
59368
+ };
59369
+ };
59370
+ const createRemotionCompositionOwnershipAnalyzer = (context) => createRemotionCompositionAttributeOwnershipAnalyzer(context, "component");
59371
+ const createRemotionMetadataOwnershipAnalyzer = (context) => createRemotionCompositionAttributeOwnershipAnalyzer(context, "calculateMetadata");
59372
+ //#endregion
59373
+ //#region src/plugin/rules/correctness/remotion-calculate-metadata-fetch-signal.ts
59374
+ const getParameterIdentifier = (parameter) => {
59375
+ if (isNodeOfType(parameter, "Identifier")) return parameter;
59376
+ if (isNodeOfType(parameter, "AssignmentPattern") && isNodeOfType(parameter.left, "Identifier")) return parameter.left;
59377
+ return null;
59378
+ };
59379
+ const getAbortSignalBinding = (functionNode) => {
59380
+ if (!isFunctionLike$1(functionNode)) return {
59381
+ parameterIdentifier: null,
59382
+ signalIdentifier: null
59383
+ };
59384
+ const firstParameter = functionNode.params[0];
59385
+ const parameterIdentifier = getParameterIdentifier(firstParameter);
59386
+ if (parameterIdentifier) return {
59387
+ parameterIdentifier,
59388
+ signalIdentifier: null
59389
+ };
59390
+ if (!isNodeOfType(firstParameter, "ObjectPattern")) return {
59391
+ parameterIdentifier: null,
59392
+ signalIdentifier: null
59393
+ };
59394
+ for (const property of firstParameter.properties) {
59395
+ if (!isNodeOfType(property, "Property")) continue;
59396
+ if (getStaticPropertyKeyName(property) !== "abortSignal") continue;
59397
+ const signalIdentifier = getParameterIdentifier(property.value);
59398
+ if (signalIdentifier) return {
59399
+ parameterIdentifier: null,
59400
+ signalIdentifier
59401
+ };
59402
+ }
59403
+ return {
59404
+ parameterIdentifier: null,
59405
+ signalIdentifier: null
59406
+ };
59407
+ };
59408
+ const identifiersResolveToSameSymbol = (leftIdentifier, rightIdentifier, scopes) => {
59409
+ if (!isNodeOfType(leftIdentifier, "Identifier") || !isNodeOfType(rightIdentifier, "Identifier")) return false;
59410
+ const leftSymbol = scopes.symbolFor(leftIdentifier);
59411
+ return Boolean(leftSymbol && leftSymbol === scopes.symbolFor(rightIdentifier));
59412
+ };
59413
+ const isMetadataAbortSignal = (expression, binding, scopes) => {
59414
+ const candidate = stripParenExpression(expression);
59415
+ if (binding.signalIdentifier && identifiersResolveToSameSymbol(candidate, binding.signalIdentifier, scopes)) return true;
59416
+ return Boolean(binding.parameterIdentifier && isNodeOfType(candidate, "MemberExpression") && getStaticPropertyKeyName(candidate, { allowComputedString: true }) === "abortSignal" && identifiersResolveToSameSymbol(candidate.object, binding.parameterIdentifier, scopes));
59417
+ };
59418
+ const fetchUsesMetadataAbortSignal = (fetchCall, binding, scopes) => {
59419
+ const optionsArgument = fetchCall.arguments[1];
59420
+ if (!optionsArgument) return false;
59421
+ const options = stripParenExpression(optionsArgument);
59422
+ if (!isNodeOfType(options, "ObjectExpression")) return null;
59423
+ if (options.properties.some((property) => isNodeOfType(property, "SpreadElement"))) return null;
59424
+ for (let propertyIndex = options.properties.length - 1; propertyIndex >= 0; propertyIndex -= 1) {
59425
+ const property = options.properties[propertyIndex];
59426
+ if (isNodeOfType(property, "Property") && getStaticPropertyKeyName(property, { allowComputedString: true }) === "signal") return isMetadataAbortSignal(property.value, binding, scopes);
59427
+ }
59428
+ return false;
59429
+ };
59430
+ const reportFetchesWithoutAbortSignal = (functionNode, context) => {
59431
+ const binding = getAbortSignalBinding(functionNode);
59432
+ walkAst(functionNode, (candidate) => {
59433
+ if (candidate !== functionNode && isFunctionLike$1(candidate)) return false;
59434
+ if (!isNodeOfType(candidate, "CallExpression") || !isNodeOfType(candidate.callee, "Identifier") || candidate.callee.name !== "fetch" || !context.scopes.isGlobalReference(candidate.callee) || fetchUsesMetadataAbortSignal(candidate, binding, context.scopes) !== false) return;
59435
+ context.report({
59436
+ node: candidate,
59437
+ message: "Pass Remotion's `abortSignal` to this fetch with `{signal: abortSignal}` so superseded metadata requests are cancelled."
59438
+ });
59439
+ });
59440
+ };
59441
+ const isCalculateMetadataFunctionType = (variableDeclarator) => {
59442
+ if (!isNodeOfType(variableDeclarator.id, "Identifier")) return false;
59443
+ const annotation = variableDeclarator.id.typeAnnotation;
59444
+ if (!isNodeOfType(annotation, "TSTypeAnnotation") || !isNodeOfType(annotation.typeAnnotation, "TSTypeReference")) return false;
59445
+ const typeName = annotation.typeAnnotation.typeName;
59446
+ if (!isNodeOfType(typeName, "Identifier")) return false;
59447
+ const apiBinding = getImportBindingForName(typeName, typeName.name);
59448
+ return Boolean(apiBinding?.exportedName === "CalculateMetadataFunction" && apiBinding.source === "remotion");
59449
+ };
59450
+ const remotionCalculateMetadataFetchSignal = defineRule({
59451
+ id: "remotion-calculate-metadata-fetch-signal",
59452
+ title: "calculateMetadata fetch ignores abortSignal",
59453
+ tags: ["react-jsx-only"],
59454
+ requires: ["remotion:4"],
59455
+ severity: "error",
59456
+ recommendation: "Destructure `abortSignal` from the calculateMetadata argument and pass it to direct fetch calls as `{signal: abortSignal}`.",
59457
+ create: (context) => {
59458
+ const analyzedFunctions = /* @__PURE__ */ new WeakSet();
59459
+ const isOwnedByCalculateMetadata = createRemotionMetadataOwnershipAnalyzer(context);
59460
+ const analyzeFunction = (functionNode) => {
59461
+ if (!functionNode || analyzedFunctions.has(functionNode)) return;
59462
+ analyzedFunctions.add(functionNode);
59463
+ reportFetchesWithoutAbortSignal(functionNode, context);
59464
+ };
59465
+ return {
59466
+ CallExpression(node) {
59467
+ if (!isNodeOfType(node.callee, "Identifier") || node.callee.name !== "fetch" || !context.scopes.isGlobalReference(node.callee)) return;
59468
+ const functionNode = findEnclosingFunction$1(node);
59469
+ if (functionNode && isOwnedByCalculateMetadata(functionNode)) analyzeFunction(functionNode);
59470
+ },
59471
+ JSXOpeningElement(node) {
59472
+ const apiBinding = resolveRemotionApi(node.name, context.scopes);
59473
+ if (apiBinding?.apiName !== "Composition" || apiBinding.moduleSource !== "remotion") return;
59474
+ const calculateMetadataAttribute = findJsxAttribute(node.attributes, "calculateMetadata");
59475
+ if (!calculateMetadataAttribute?.value || !isNodeOfType(calculateMetadataAttribute.value, "JSXExpressionContainer") || !calculateMetadataAttribute.value.expression) return;
59476
+ analyzeFunction(resolveExactLocalFunction(calculateMetadataAttribute.value.expression, context.scopes));
59477
+ },
59478
+ VariableDeclarator(node) {
59479
+ if (!node.init || !isCalculateMetadataFunctionType(node)) return;
59480
+ analyzeFunction(resolveExactLocalFunction(node.init, context.scopes));
59481
+ }
59482
+ };
59483
+ }
59484
+ });
59485
+ //#endregion
59486
+ //#region src/plugin/utils/create-remotion-render-evidence-checker.ts
59487
+ const REMOTION_RENDER_CALL_NAMES = new Set([
59488
+ "continueRender",
59489
+ "delayRender",
59490
+ "getInputProps",
59491
+ "random",
59492
+ "spring",
59493
+ "useCurrentFrame",
59494
+ "useDelayRender",
59495
+ "useVideoConfig"
59496
+ ]);
59497
+ const REMOTION_RENDER_COMPONENT_MODULE_BY_NAME = new Map([
59498
+ ["Audio", "@remotion/media"],
59499
+ ["Freeze", "remotion"],
59500
+ ["IFrame", "remotion"],
59501
+ ["Img", "remotion"],
59502
+ ["Loop", "remotion"],
59503
+ ["OffthreadVideo", "remotion"],
59504
+ ["Sequence", "remotion"],
59505
+ ["Series", "remotion"],
59506
+ ["Video", "@remotion/media"]
59507
+ ]);
59508
+ const createRemotionRenderEvidenceChecker = (context) => {
59509
+ const scopes = context.scopes;
59510
+ const evidenceByFunction = /* @__PURE__ */ new WeakMap();
59511
+ const registeredCompositionFunctions = /* @__PURE__ */ new WeakSet();
59512
+ const inspectedPrograms = /* @__PURE__ */ new WeakSet();
59513
+ const isOwnedByRegisteredComposition = createRemotionCompositionOwnershipAnalyzer(context);
59514
+ const collectRegisteredCompositionFunctions = (functionNode) => {
59515
+ const program = findProgramRoot(functionNode);
59516
+ if (!program || inspectedPrograms.has(program)) return;
59517
+ inspectedPrograms.add(program);
59518
+ walkAst(program, (candidate) => {
59519
+ if (!isNodeOfType(candidate, "JSXOpeningElement")) return;
59520
+ const apiBinding = resolveRemotionApi(candidate.name, scopes);
59521
+ if (apiBinding?.apiName !== "Composition" || apiBinding.moduleSource !== "remotion") return;
59522
+ const componentAttribute = findJsxAttribute(candidate.attributes, "component");
59523
+ if (!componentAttribute?.value || !isNodeOfType(componentAttribute.value, "JSXExpressionContainer") || !componentAttribute.value.expression) return;
59524
+ const registeredFunction = resolveExactLocalFunction(componentAttribute.value.expression, scopes);
59525
+ if (registeredFunction) registeredCompositionFunctions.add(registeredFunction);
59526
+ });
59527
+ };
59528
+ const functionUsesRemotionRenderApi = (functionNode) => {
59529
+ let hasEvidence = false;
59530
+ walkAst(functionNode, (candidate) => {
59531
+ if (hasEvidence) return false;
59532
+ if (!isNodeOfType(candidate, "CallExpression") && !isNodeOfType(candidate, "JSXOpeningElement")) return;
59533
+ const apiBinding = resolveRemotionApi(isNodeOfType(candidate, "CallExpression") ? candidate.callee : candidate.name, scopes);
59534
+ if (isNodeOfType(candidate, "CallExpression") && apiBinding?.moduleSource === "remotion" && REMOTION_RENDER_CALL_NAMES.has(apiBinding.apiName) || isNodeOfType(candidate, "JSXOpeningElement") && apiBinding !== null && REMOTION_RENDER_COMPONENT_MODULE_BY_NAME.get(apiBinding.apiName) === apiBinding.moduleSource) {
59535
+ hasEvidence = true;
59536
+ return false;
59537
+ }
59538
+ });
59539
+ return hasEvidence;
59540
+ };
59541
+ return { functionHasEvidence: (functionNode) => {
59542
+ const cachedEvidence = evidenceByFunction.get(functionNode);
59543
+ if (cachedEvidence !== void 0) return cachedEvidence;
59544
+ collectRegisteredCompositionFunctions(functionNode);
59545
+ const hasEvidence = registeredCompositionFunctions.has(functionNode) || functionUsesRemotionRenderApi(functionNode) || isOwnedByRegisteredComposition(functionNode);
59546
+ evidenceByFunction.set(functionNode, hasEvidence);
59547
+ return hasEvidence;
59548
+ } };
59549
+ };
59550
+ //#endregion
59551
+ //#region src/plugin/rules/correctness/remotion-deterministic-randomness.ts
59552
+ const isGlobalMathObject = (node, scopes) => {
59553
+ const candidate = stripParenExpression(node);
59554
+ if (isNodeOfType(candidate, "Identifier")) return candidate.name === "Math" && scopes.isGlobalReference(candidate);
59555
+ return Boolean(isNodeOfType(candidate, "MemberExpression") && getStaticPropertyKeyName(candidate, { allowComputedString: true }) === "Math" && isNodeOfType(candidate.object, "Identifier") && candidate.object.name === "globalThis" && scopes.isGlobalReference(candidate.object));
59556
+ };
59557
+ const remotionDeterministicRandomness = defineRule({
59558
+ id: "remotion-deterministic-randomness",
59559
+ title: "Randomness changes between rendered frames",
59560
+ tags: ["react-jsx-only"],
59561
+ requires: ["remotion:4"],
59562
+ severity: "error",
59563
+ recommendation: "Use Remotion's seeded `random(seed)` helper so the same frame produces the same value in every render tab.",
59564
+ create: (context) => {
59565
+ const renderEvidence = createRemotionRenderEvidenceChecker(context);
59566
+ return { CallExpression(node) {
59567
+ const callee = stripParenExpression(node.callee);
59568
+ if (!isNodeOfType(callee, "MemberExpression") || getStaticPropertyKeyName(callee, { allowComputedString: true }) !== "random" || !isGlobalMathObject(callee.object, context.scopes)) return;
59569
+ const componentOrHook = findRenderPhaseComponentOrHook(node, context.scopes) ?? findEnclosingFunction$1(node);
59570
+ if (!componentOrHook || !renderEvidence.functionHasEvidence(componentOrHook)) return;
59571
+ const displayName = componentOrHookDisplayNameForFunction(componentOrHook);
59572
+ if (displayName && !isReactHookName(displayName) && !functionHasReactComponentEvidence(componentOrHook, context.scopes, context.cfg)) return;
59573
+ context.report({
59574
+ node,
59575
+ message: "`Math.random()` can return a different value in each parallel Remotion render tab, so the same frame is not deterministic. Use `random(seed)` from `remotion` instead."
59576
+ });
59577
+ } };
59578
+ }
59579
+ });
59580
+ //#endregion
59581
+ //#region src/plugin/utils/create-remotion-css-time-rule-visitors.ts
59582
+ const createRemotionCssTimeRuleVisitors = (context, options) => {
59583
+ const renderEvidence = createRemotionRenderEvidenceChecker(context);
59584
+ return {
59585
+ Property(node) {
59586
+ if (!options.stylePropertyNames.has(getStaticPropertyKeyName(node) ?? "")) return;
59587
+ const styleObject = node.parent;
59588
+ const expressionContainer = styleObject?.parent;
59589
+ const styleAttribute = expressionContainer?.parent;
59590
+ if (!isNodeOfType(styleObject, "ObjectExpression") || !isNodeOfType(expressionContainer, "JSXExpressionContainer") || expressionContainer.expression !== styleObject || !isNodeOfType(styleAttribute, "JSXAttribute") || !isNodeOfType(styleAttribute.name, "JSXIdentifier") || styleAttribute.name.name !== "style") return;
59591
+ const renderFunction = findRenderPhaseComponentOrHook(node, context.scopes);
59592
+ if (!renderFunction || !renderEvidence.functionHasEvidence(renderFunction)) return;
59593
+ context.report({
59594
+ node,
59595
+ message: options.styleMessage
59596
+ });
59597
+ },
59598
+ JSXAttribute(node) {
59599
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "className") return;
59600
+ const renderFunction = findRenderPhaseComponentOrHook(node, context.scopes);
59601
+ if (!renderFunction || !renderEvidence.functionHasEvidence(renderFunction)) return;
59602
+ const className = getJsxAttributeStaticString(node);
59603
+ if (!className) return;
59604
+ if (className.split(/\s+/).filter(Boolean).some((classToken) => options.classTokenIsForbidden(classToken.split(":").at(-1) ?? ""))) context.report({
59605
+ node,
59606
+ message: options.classMessage
59607
+ });
59608
+ }
59609
+ };
59610
+ };
59611
+ //#endregion
59612
+ //#region src/plugin/rules/correctness/remotion-no-css-animation.ts
59613
+ const ANIMATION_STYLE_PROPERTY_NAMES = new Set(["animation", "animationName"]);
59614
+ const remotionNoCssAnimation = defineRule({
59615
+ id: "remotion-no-css-animation",
59616
+ title: "CSS animation is not frame-driven",
59617
+ tags: ["react-jsx-only"],
59618
+ requires: ["remotion:4"],
59619
+ severity: "error",
59620
+ recommendation: "Drive the property from `useCurrentFrame()` with `interpolate()` so every rendered frame is deterministic.",
59621
+ create: (context) => createRemotionCssTimeRuleVisitors(context, {
59622
+ classTokenIsForbidden: (classToken) => classToken.startsWith("animate-") && classToken !== "animate-none",
59623
+ classMessage: "Tailwind animations advance on browser time, so Remotion can capture inconsistent frames. Drive the property from `useCurrentFrame()` instead.",
59624
+ styleMessage: "CSS animations advance on browser time, so Remotion can capture inconsistent frames. Drive the property from `useCurrentFrame()` instead.",
59625
+ stylePropertyNames: ANIMATION_STYLE_PROPERTY_NAMES
59626
+ })
59627
+ });
59628
+ //#endregion
59629
+ //#region src/plugin/rules/correctness/remotion-no-css-transition.ts
59630
+ const TRANSITION_STYLE_PROPERTY_NAMES = new Set(["transition", "transitionProperty"]);
59631
+ const remotionNoCssTransition = defineRule({
59632
+ id: "remotion-no-css-transition",
59633
+ title: "CSS transition is not frame-driven",
59634
+ tags: ["react-jsx-only"],
59635
+ requires: ["remotion:4"],
59636
+ severity: "error",
59637
+ recommendation: "Drive the property from `useCurrentFrame()` with `interpolate()` so every rendered frame is deterministic.",
59638
+ create: (context) => createRemotionCssTimeRuleVisitors(context, {
59639
+ classTokenIsForbidden: (classToken) => (classToken === "transition" || classToken.startsWith("transition-")) && classToken !== "transition-none",
59640
+ classMessage: "Tailwind transitions advance on browser time, so Remotion can capture inconsistent frames. Drive the property from `useCurrentFrame()` instead.",
59641
+ styleMessage: "CSS transitions advance on browser time, so Remotion can capture inconsistent frames. Drive the property from `useCurrentFrame()` instead.",
59642
+ stylePropertyNames: TRANSITION_STYLE_PROPERTY_NAMES
59643
+ })
59644
+ });
59645
+ //#endregion
59646
+ //#region src/plugin/rules/correctness/remotion-no-css-url-assets.ts
59647
+ const CSS_URL_ASSET_PROPERTY_NAMES = new Set([
59648
+ "backgroundImage",
59649
+ "maskImage",
59650
+ "WebkitMaskImage"
59651
+ ]);
59652
+ const CSS_URL_PATTERN = /\burl\(\s*(["']?)([^"')]+)\1\s*\)/i;
59653
+ const isEmbeddedAssetSource = (assetSource) => assetSource.startsWith("data:") || assetSource.startsWith("#");
59654
+ const getStaticStringExpression = (node) => {
59655
+ if (isNodeOfType(node, "Literal") && typeof node.value === "string") return node.value;
59656
+ if (isNodeOfType(node, "TemplateLiteral") && node.expressions.length === 0 && node.quasis.length === 1) return node.quasis[0].value.raw;
59657
+ return null;
59658
+ };
59659
+ const isReactUseMemoCallback = (node, scopes) => {
59660
+ const parent = node.parent;
59661
+ return Boolean(isNodeOfType(parent, "CallExpression") && parent.arguments[0] === node && isReactApiCall(parent, "useMemo", scopes, { resolveNamedAliases: true }));
59662
+ };
59663
+ const componentPreloadsStaticImage = (componentNode, assetSource, scopes) => {
59664
+ let hasPreload = false;
59665
+ walkAst(componentNode, (child) => {
59666
+ if (hasPreload) return false;
59667
+ if (child !== componentNode && isFunctionLike$1(child) && !isReactUseMemoCallback(child, scopes)) return false;
59668
+ if (!isNodeOfType(child, "JSXOpeningElement")) return;
59669
+ const apiBinding = resolveRemotionApi(child.name, scopes);
59670
+ if (apiBinding?.apiName !== "Img" || apiBinding.moduleSource !== "remotion") return;
59671
+ const sourceAttribute = findJsxAttribute(child.attributes, "src");
59672
+ if (sourceAttribute && getJsxAttributeStaticString(sourceAttribute) === assetSource) {
59673
+ hasPreload = true;
59674
+ return false;
59675
+ }
59676
+ });
59677
+ return hasPreload;
59678
+ };
59679
+ const remotionNoCssUrlAssets = defineRule({
59680
+ id: "remotion-no-css-url-assets",
59681
+ title: "CSS URL asset can flicker in Remotion",
59682
+ tags: ["react-jsx-only"],
59683
+ requires: ["remotion:4"],
59684
+ severity: "error",
59685
+ recommendation: "Render the asset with `Img` inside an `AbsoluteFill`, or preload the same source with a hidden `Img` when a CSS mask is required.",
59686
+ create: (context) => {
59687
+ const renderEvidence = createRemotionRenderEvidenceChecker(context);
59688
+ return { Property(node) {
59689
+ if (!CSS_URL_ASSET_PROPERTY_NAMES.has(getStaticPropertyKeyName(node) ?? "")) return;
59690
+ const styleObject = node.parent;
59691
+ const expressionContainer = styleObject?.parent;
59692
+ const styleAttribute = expressionContainer?.parent;
59693
+ if (!isNodeOfType(styleObject, "ObjectExpression") || !isNodeOfType(expressionContainer, "JSXExpressionContainer") || expressionContainer.expression !== styleObject || !isNodeOfType(styleAttribute, "JSXAttribute") || !isNodeOfType(styleAttribute.name, "JSXIdentifier") || styleAttribute.name.name !== "style") return;
59694
+ const cssValue = getStaticStringExpression(node.value);
59695
+ const urlMatch = cssValue ? CSS_URL_PATTERN.exec(cssValue) : null;
59696
+ if (!urlMatch) return;
59697
+ const assetSource = urlMatch[2].trim();
59698
+ if (isEmbeddedAssetSource(assetSource)) return;
59699
+ const componentNode = findRenderPhaseComponentOrHook(node, context.scopes);
59700
+ if (!componentNode || !renderEvidence.functionHasEvidence(componentNode) || componentPreloadsStaticImage(componentNode, assetSource, context.scopes)) return;
59701
+ context.report({
59702
+ node,
59703
+ message: "Remotion cannot detect when a CSS `url()` asset has loaded, so the rendered frame can flicker. Render or preload the source with <Img> instead."
59704
+ });
59705
+ } };
59706
+ }
59707
+ });
59708
+ //#endregion
59709
+ //#region src/plugin/rules/correctness/remotion-no-module-scope-delay-render.ts
59710
+ const remotionNoModuleScopeDelayRender = defineRule({
59711
+ id: "remotion-no-module-scope-delay-render",
59712
+ title: "Module-scoped delayRender blocks every composition",
59713
+ requires: ["remotion:4"],
59714
+ severity: "error",
59715
+ recommendation: "Create the handle once inside the component. Use `useDelayRender()` on Remotion 4.0.342 or newer, or lazy `useState(() => delayRender())` on earlier versions.",
59716
+ create: (context) => ({ CallExpression(node) {
59717
+ const apiBinding = resolveRemotionApi(node.callee, context.scopes);
59718
+ if (apiBinding?.apiName !== "delayRender" || apiBinding.moduleSource !== "remotion" || findEnclosingFunction$1(node)) return;
59719
+ context.report({
59720
+ node,
59721
+ message: "A module-scoped `delayRender()` handle blocks all compositions and composition discovery. Move it inside the component and create it once with `useDelayRender()` or a lazy `useState` initializer."
59722
+ });
59723
+ } })
59724
+ });
59725
+ //#endregion
59726
+ //#region src/plugin/rules/correctness/remotion-no-native-media-elements.ts
59727
+ const REMOTION_MEDIA_REPLACEMENT_BY_TAG = new Map([
59728
+ ["audio", "`Audio` from `@remotion/media`"],
59729
+ ["iframe", "`IFrame` from `remotion`"],
59730
+ ["img", "`Img` from `remotion`"],
59731
+ ["video", "`Video` from `@remotion/media`"]
59732
+ ]);
59733
+ const remotionNoNativeMediaElements = defineRule({
59734
+ id: "remotion-no-native-media-elements",
59735
+ title: "Native media element bypasses Remotion loading",
59736
+ tags: ["react-jsx-only"],
59737
+ requires: ["remotion:4"],
59738
+ severity: "error",
59739
+ recommendation: "Use Remotion's media components so rendering waits for assets and seeks media to the requested frame.",
59740
+ create: (context) => {
59741
+ const renderEvidence = createRemotionRenderEvidenceChecker(context);
59742
+ return { JSXOpeningElement(node) {
59743
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
59744
+ const renderFunction = findRenderPhaseComponentOrHook(node, context.scopes);
59745
+ if (!renderFunction || !renderEvidence.functionHasEvidence(renderFunction)) return;
59746
+ const replacement = REMOTION_MEDIA_REPLACEMENT_BY_TAG.get(node.name.name);
59747
+ if (!replacement) return;
59748
+ context.report({
59749
+ node,
59750
+ message: `Native <${node.name.name}> does not let Remotion reliably wait for and synchronize the asset. Use ${replacement} instead.`
59751
+ });
59752
+ } };
59753
+ }
59754
+ });
59755
+ //#endregion
59756
+ //#region src/plugin/rules/correctness/remotion-no-next-image.ts
59757
+ const remotionNoNextImage = defineRule({
59758
+ id: "remotion-no-next-image",
59759
+ title: "Next.js Image can flicker in Remotion",
59760
+ tags: ["react-jsx-only"],
59761
+ requires: ["remotion:4"],
59762
+ severity: "error",
59763
+ recommendation: "Use `Img` from `remotion`, which delays rendering until the image is loaded.",
59764
+ create: (context) => {
59765
+ const renderEvidence = createRemotionRenderEvidenceChecker(context);
59766
+ return { JSXOpeningElement(node) {
59767
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
59768
+ const renderFunction = findRenderPhaseComponentOrHook(node, context.scopes);
59769
+ if (!renderFunction || !renderEvidence.functionHasEvidence(renderFunction)) return;
59770
+ const symbol = context.scopes.symbolFor(node.name);
59771
+ if (symbol?.kind !== "import") return;
59772
+ const importBinding = getImportBindingForName(node.name, symbol.name);
59773
+ if (importBinding?.source !== "next/image" || importBinding.exportedName !== "default") return;
59774
+ context.report({
59775
+ node,
59776
+ message: "Next.js <Image> does not expose a reliable loading signal to Remotion, so rendered frames can flicker. Use <Img> from `remotion` instead."
59777
+ });
59778
+ } };
59779
+ }
59780
+ });
59781
+ //#endregion
59782
+ //#region src/plugin/rules/correctness/remotion-stable-delay-render-handle.ts
59783
+ const isUseStateLazyInitializer = (node, scopes) => {
59784
+ const enclosingFunction = findEnclosingFunction$1(node);
59785
+ if (!enclosingFunction) return false;
59786
+ const parent = enclosingFunction.parent;
59787
+ return Boolean(isNodeOfType(parent, "CallExpression") && parent.arguments[0] === enclosingFunction && isReactApiCall(parent, "useState", scopes, { resolveNamedAliases: true }));
59788
+ };
59789
+ const remotionStableDelayRenderHandle = defineRule({
59790
+ id: "remotion-stable-delay-render-handle",
59791
+ title: "delayRender handle is recreated during render",
59792
+ tags: ["react-jsx-only"],
59793
+ requires: ["remotion:4"],
59794
+ severity: "error",
59795
+ recommendation: "Prefer `useDelayRender()`, or initialize `delayRender()` once with `useState(() => delayRender())`.",
59796
+ create: (context) => ({ CallExpression(node) {
59797
+ const apiBinding = resolveRemotionApi(node.callee, context.scopes);
59798
+ if (apiBinding?.apiName !== "delayRender" || apiBinding.moduleSource !== "remotion" || !findRenderPhaseComponentOrHook(node, context.scopes) || isUseStateLazyInitializer(node, context.scopes)) return;
59799
+ context.report({
59800
+ node,
59801
+ message: "Calling `delayRender()` during every component render creates another outstanding handle and can make rendering time out. Use `useDelayRender()` or a lazy `useState` initializer."
59802
+ });
59803
+ } })
59804
+ });
59805
+ //#endregion
58742
59806
  //#region src/plugin/rules/performance/rendering-animate-svg-wrapper.ts
58743
59807
  const renderingAnimateSvgWrapper = defineRule({
58744
59808
  id: "rendering-animate-svg-wrapper",
@@ -73902,6 +74966,39 @@ const reactDoctorRules = [
73902
74966
  requires: [...new Set(["react", ...mediaHasCaption.requires ?? []])]
73903
74967
  }
73904
74968
  },
74969
+ {
74970
+ key: "react-doctor/mobx-no-make-auto-observable-in-inheritance",
74971
+ id: "mobx-no-make-auto-observable-in-inheritance",
74972
+ source: "react-doctor",
74973
+ originallyExternal: false,
74974
+ rule: {
74975
+ ...mobxNoMakeAutoObservableInInheritance,
74976
+ framework: "global",
74977
+ category: "Bugs"
74978
+ }
74979
+ },
74980
+ {
74981
+ key: "react-doctor/mobx-no-observer-wrapped-memo",
74982
+ id: "mobx-no-observer-wrapped-memo",
74983
+ source: "react-doctor",
74984
+ originallyExternal: false,
74985
+ rule: {
74986
+ ...mobxNoObserverWrappedMemo,
74987
+ framework: "global",
74988
+ category: "Bugs"
74989
+ }
74990
+ },
74991
+ {
74992
+ key: "react-doctor/mobx-reaction-disposer-discarded",
74993
+ id: "mobx-reaction-disposer-discarded",
74994
+ source: "react-doctor",
74995
+ originallyExternal: false,
74996
+ rule: {
74997
+ ...mobxReactionDisposerDiscarded,
74998
+ framework: "global",
74999
+ category: "Bugs"
75000
+ }
75001
+ },
73905
75002
  {
73906
75003
  key: "react-doctor/mouse-events-have-key-events",
73907
75004
  id: "mouse-events-have-key-events",
@@ -76084,6 +77181,105 @@ const reactDoctorRules = [
76084
77181
  requires: [...new Set(["react", ...reduxUseselectorReturnsNewCollection.requires ?? []])]
76085
77182
  }
76086
77183
  },
77184
+ {
77185
+ key: "react-doctor/remotion-calculate-metadata-fetch-signal",
77186
+ id: "remotion-calculate-metadata-fetch-signal",
77187
+ source: "react-doctor",
77188
+ originallyExternal: false,
77189
+ rule: {
77190
+ ...remotionCalculateMetadataFetchSignal,
77191
+ framework: "global",
77192
+ category: "Bugs"
77193
+ }
77194
+ },
77195
+ {
77196
+ key: "react-doctor/remotion-deterministic-randomness",
77197
+ id: "remotion-deterministic-randomness",
77198
+ source: "react-doctor",
77199
+ originallyExternal: false,
77200
+ rule: {
77201
+ ...remotionDeterministicRandomness,
77202
+ framework: "global",
77203
+ category: "Bugs"
77204
+ }
77205
+ },
77206
+ {
77207
+ key: "react-doctor/remotion-no-css-animation",
77208
+ id: "remotion-no-css-animation",
77209
+ source: "react-doctor",
77210
+ originallyExternal: false,
77211
+ rule: {
77212
+ ...remotionNoCssAnimation,
77213
+ framework: "global",
77214
+ category: "Bugs"
77215
+ }
77216
+ },
77217
+ {
77218
+ key: "react-doctor/remotion-no-css-transition",
77219
+ id: "remotion-no-css-transition",
77220
+ source: "react-doctor",
77221
+ originallyExternal: false,
77222
+ rule: {
77223
+ ...remotionNoCssTransition,
77224
+ framework: "global",
77225
+ category: "Bugs"
77226
+ }
77227
+ },
77228
+ {
77229
+ key: "react-doctor/remotion-no-css-url-assets",
77230
+ id: "remotion-no-css-url-assets",
77231
+ source: "react-doctor",
77232
+ originallyExternal: false,
77233
+ rule: {
77234
+ ...remotionNoCssUrlAssets,
77235
+ framework: "global",
77236
+ category: "Bugs"
77237
+ }
77238
+ },
77239
+ {
77240
+ key: "react-doctor/remotion-no-module-scope-delay-render",
77241
+ id: "remotion-no-module-scope-delay-render",
77242
+ source: "react-doctor",
77243
+ originallyExternal: false,
77244
+ rule: {
77245
+ ...remotionNoModuleScopeDelayRender,
77246
+ framework: "global",
77247
+ category: "Bugs"
77248
+ }
77249
+ },
77250
+ {
77251
+ key: "react-doctor/remotion-no-native-media-elements",
77252
+ id: "remotion-no-native-media-elements",
77253
+ source: "react-doctor",
77254
+ originallyExternal: false,
77255
+ rule: {
77256
+ ...remotionNoNativeMediaElements,
77257
+ framework: "global",
77258
+ category: "Bugs"
77259
+ }
77260
+ },
77261
+ {
77262
+ key: "react-doctor/remotion-no-next-image",
77263
+ id: "remotion-no-next-image",
77264
+ source: "react-doctor",
77265
+ originallyExternal: false,
77266
+ rule: {
77267
+ ...remotionNoNextImage,
77268
+ framework: "global",
77269
+ category: "Bugs"
77270
+ }
77271
+ },
77272
+ {
77273
+ key: "react-doctor/remotion-stable-delay-render-handle",
77274
+ id: "remotion-stable-delay-render-handle",
77275
+ source: "react-doctor",
77276
+ originallyExternal: false,
77277
+ rule: {
77278
+ ...remotionStableDelayRenderHandle,
77279
+ framework: "global",
77280
+ category: "Bugs"
77281
+ }
77282
+ },
76087
77283
  {
76088
77284
  key: "react-doctor/rendering-animate-svg-wrapper",
76089
77285
  id: "rendering-animate-svg-wrapper",
@@ -77567,6 +78763,13 @@ const CROSS_FILE_RULE_IDS = new Set([
77567
78763
  "no-unguarded-browser-global-in-render-or-hook-init",
77568
78764
  "prefer-dynamic-import",
77569
78765
  "rendering-hydration-mismatch-time",
78766
+ "remotion-calculate-metadata-fetch-signal",
78767
+ "remotion-deterministic-randomness",
78768
+ "remotion-no-css-animation",
78769
+ "remotion-no-css-transition",
78770
+ "remotion-no-css-url-assets",
78771
+ "remotion-no-native-media-elements",
78772
+ "remotion-no-next-image",
77570
78773
  "rerender-memo-with-default-value",
77571
78774
  "rn-no-legacy-shadow-styles",
77572
78775
  "rn-no-raw-text",
@@ -77784,7 +78987,17 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
77784
78987
  * `CROSS_FILE_DEPENDENCY_COLLECTORS` (the core guard test enforces the
77785
78988
  * partition), forcing a conscious classification.
77786
78989
  */
77787
- const UNBOUNDED_CROSS_FILE_RULE_IDS = new Set(["nextjs-no-img-element", "only-export-components"]);
78990
+ const UNBOUNDED_CROSS_FILE_RULE_IDS = new Set([
78991
+ "nextjs-no-img-element",
78992
+ "only-export-components",
78993
+ "remotion-calculate-metadata-fetch-signal",
78994
+ "remotion-deterministic-randomness",
78995
+ "remotion-no-css-animation",
78996
+ "remotion-no-css-transition",
78997
+ "remotion-no-css-url-assets",
78998
+ "remotion-no-native-media-elements",
78999
+ "remotion-no-next-image"
79000
+ ]);
77788
79001
  /**
77789
79002
  * Runs the collectors for `ruleIds` over one file and returns every
77790
79003
  * filesystem probe they made — the file's cross-file dependency set.