oxlint-plugin-react-doctor 0.4.0-dev.a5c6993 → 0.4.0-dev.b1a22ef

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 +114 -0
  2. package/dist/index.js +1126 -617
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as path from "node:path";
2
- import { analyze } from "eslint-scope";
3
- import * as eslintVisitorKeys from "eslint-visitor-keys";
4
2
  import * as fs from "node:fs";
5
3
  import { parseSync } from "oxc-parser";
4
+ import { analyze } from "eslint-scope";
5
+ import * as eslintVisitorKeys from "eslint-visitor-keys";
6
6
  //#region src/plugin/utils/is-testlike-filename.ts
7
7
  const NON_PRODUCTION_PATH_SEGMENTS = [
8
8
  "/test/",
@@ -829,6 +829,9 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
829
829
  return /^(opengraph-image|twitter-image|icon|apple-icon)\d*\.(jsx?|tsx?)$/.test(path.basename(rawFilename));
830
830
  };
831
831
  //#endregion
832
+ //#region src/plugin/utils/normalize-filename.ts
833
+ const normalizeFilename$1 = (filename) => filename.replaceAll("\\", "/");
834
+ //#endregion
832
835
  //#region src/plugin/utils/is-hidden-from-screen-reader.ts
833
836
  const isHiddenFromScreenReader = (openingElement, settings) => {
834
837
  if (getElementType(openingElement, settings).toLowerCase() === "input") {
@@ -1000,7 +1003,7 @@ const altText = defineRule({
1000
1003
  recommendation: "Give every meaningful image an `alt`, `aria-label`, or `aria-labelledby`.",
1001
1004
  category: "Accessibility",
1002
1005
  create: (context) => {
1003
- if (isNextjsMetadataImageRouteFilename(context.filename)) return {};
1006
+ if (isNextjsMetadataImageRouteFilename(normalizeFilename$1(context.filename ?? ""))) return {};
1004
1007
  const settings = resolveSettings$53(context.settings);
1005
1008
  const checkImg = !settings.elements || settings.elements.includes("img");
1006
1009
  const checkObject = !settings.elements || settings.elements.includes("object");
@@ -1102,7 +1105,7 @@ const anchorAmbiguousText = defineRule({
1102
1105
  });
1103
1106
  //#endregion
1104
1107
  //#region src/plugin/rules/a11y/anchor-has-content.ts
1105
- const MESSAGE$50 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1108
+ const MESSAGE$51 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1106
1109
  const anchorHasContent = defineRule({
1107
1110
  id: "anchor-has-content",
1108
1111
  title: "Anchor has no content",
@@ -1118,7 +1121,7 @@ const anchorHasContent = defineRule({
1118
1121
  for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
1119
1122
  context.report({
1120
1123
  node: opening.name,
1121
- message: MESSAGE$50
1124
+ message: MESSAGE$51
1122
1125
  });
1123
1126
  } })
1124
1127
  });
@@ -1512,7 +1515,7 @@ const parseJsxValue = (value) => {
1512
1515
  };
1513
1516
  //#endregion
1514
1517
  //#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
1515
- const MESSAGE$49 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
1518
+ const MESSAGE$50 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
1516
1519
  const ariaActivedescendantHasTabindex = defineRule({
1517
1520
  id: "aria-activedescendant-has-tabindex",
1518
1521
  title: "aria-activedescendant missing tabindex",
@@ -1530,14 +1533,14 @@ const ariaActivedescendantHasTabindex = defineRule({
1530
1533
  if (tabIndexValue === null || tabIndexValue >= -1) return;
1531
1534
  context.report({
1532
1535
  node: node.name,
1533
- message: MESSAGE$49
1536
+ message: MESSAGE$50
1534
1537
  });
1535
1538
  return;
1536
1539
  }
1537
1540
  if (isInteractiveElement(tag, node)) return;
1538
1541
  context.report({
1539
1542
  node: node.name,
1540
- message: MESSAGE$49
1543
+ message: MESSAGE$50
1541
1544
  });
1542
1545
  } })
1543
1546
  });
@@ -3073,9 +3076,6 @@ const asyncDeferAwait = defineRule({
3073
3076
  }
3074
3077
  });
3075
3078
  //#endregion
3076
- //#region src/plugin/utils/normalize-filename.ts
3077
- const normalizeFilename$1 = (filename) => filename.replaceAll("\\", "/");
3078
- //#endregion
3079
3079
  //#region src/plugin/utils/get-callee-identifier-trail.ts
3080
3080
  const getCalleeIdentifierTrail = (call) => {
3081
3081
  let entry = call;
@@ -3551,7 +3551,7 @@ const isPureEventBlockerHandler = (attribute) => {
3551
3551
  //#endregion
3552
3552
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
3553
3553
  const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
3554
- const MESSAGE$48 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
3554
+ const MESSAGE$49 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
3555
3555
  const KEY_HANDLERS = [
3556
3556
  "onKeyUp",
3557
3557
  "onKeyDown",
@@ -3583,7 +3583,7 @@ const clickEventsHaveKeyEvents = defineRule({
3583
3583
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
3584
3584
  context.report({
3585
3585
  node: node.name,
3586
- message: MESSAGE$48
3586
+ message: MESSAGE$49
3587
3587
  });
3588
3588
  } };
3589
3589
  }
@@ -3696,7 +3696,7 @@ const stripParenExpression = (node) => {
3696
3696
  };
3697
3697
  //#endregion
3698
3698
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
3699
- const MESSAGE$47 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
3699
+ const MESSAGE$48 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
3700
3700
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
3701
3701
  const DEFAULT_LABELLING_PROPS = [
3702
3702
  "alt",
@@ -3857,7 +3857,7 @@ const controlHasAssociatedLabel = defineRule({
3857
3857
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
3858
3858
  context.report({
3859
3859
  node: opening,
3860
- message: MESSAGE$47
3860
+ message: MESSAGE$48
3861
3861
  });
3862
3862
  } };
3863
3863
  }
@@ -4193,7 +4193,7 @@ const isEs6Component = (node) => {
4193
4193
  };
4194
4194
  //#endregion
4195
4195
  //#region src/plugin/rules/react-builtins/display-name.ts
4196
- const MESSAGE$46 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
4196
+ const MESSAGE$47 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
4197
4197
  const DEFAULT_ADDITIONAL_HOCS = [
4198
4198
  "observer",
4199
4199
  "lazy",
@@ -4396,7 +4396,7 @@ const displayName = defineRule({
4396
4396
  const reportAt = (node) => {
4397
4397
  context.report({
4398
4398
  node,
4399
- message: MESSAGE$46
4399
+ message: MESSAGE$47
4400
4400
  });
4401
4401
  };
4402
4402
  return {
@@ -6507,7 +6507,7 @@ const forbidElements = defineRule({
6507
6507
  });
6508
6508
  //#endregion
6509
6509
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
6510
- const MESSAGE$45 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
6510
+ const MESSAGE$46 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
6511
6511
  const forwardRefUsesRef = defineRule({
6512
6512
  id: "forward-ref-uses-ref",
6513
6513
  title: "forwardRef without ref parameter",
@@ -6527,13 +6527,13 @@ const forwardRefUsesRef = defineRule({
6527
6527
  if (isNodeOfType(onlyParam, "RestElement")) return;
6528
6528
  context.report({
6529
6529
  node: inner,
6530
- message: MESSAGE$45
6530
+ message: MESSAGE$46
6531
6531
  });
6532
6532
  } })
6533
6533
  });
6534
6534
  //#endregion
6535
6535
  //#region src/plugin/rules/a11y/heading-has-content.ts
6536
- const MESSAGE$44 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
6536
+ const MESSAGE$45 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
6537
6537
  const DEFAULT_HEADING_TAGS = [
6538
6538
  "h1",
6539
6539
  "h2",
@@ -6566,7 +6566,7 @@ const headingHasContent = defineRule({
6566
6566
  if (isHiddenFromScreenReader(node, context.settings)) return;
6567
6567
  context.report({
6568
6568
  node,
6569
- message: MESSAGE$44
6569
+ message: MESSAGE$45
6570
6570
  });
6571
6571
  } };
6572
6572
  }
@@ -6704,7 +6704,7 @@ const hooksNoNanInDeps = defineRule({
6704
6704
  });
6705
6705
  //#endregion
6706
6706
  //#region src/plugin/rules/a11y/html-has-lang.ts
6707
- const MESSAGE$43 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
6707
+ const MESSAGE$44 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
6708
6708
  const resolveSettings$39 = (settings) => {
6709
6709
  const reactDoctor = settings?.["react-doctor"];
6710
6710
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -6752,7 +6752,7 @@ const htmlHasLang = defineRule({
6752
6752
  if (!lang) {
6753
6753
  context.report({
6754
6754
  node: node.name,
6755
- message: MESSAGE$43
6755
+ message: MESSAGE$44
6756
6756
  });
6757
6757
  return;
6758
6758
  }
@@ -6760,13 +6760,13 @@ const htmlHasLang = defineRule({
6760
6760
  if (verdict === "missing" || verdict === "empty") {
6761
6761
  context.report({
6762
6762
  node: lang,
6763
- message: MESSAGE$43
6763
+ message: MESSAGE$44
6764
6764
  });
6765
6765
  return;
6766
6766
  }
6767
6767
  if (hasSpread && !lang) context.report({
6768
6768
  node: node.name,
6769
- message: MESSAGE$43
6769
+ message: MESSAGE$44
6770
6770
  });
6771
6771
  } };
6772
6772
  }
@@ -6980,7 +6980,7 @@ const htmlNoNestedInteractive = defineRule({
6980
6980
  });
6981
6981
  //#endregion
6982
6982
  //#region src/plugin/rules/a11y/iframe-has-title.ts
6983
- const MESSAGE$42 = "Blind users can't tell what this `<iframe>` holds because screen readers have no title to read, so add a `title` describing its content.";
6983
+ const MESSAGE$43 = "Blind users can't tell what this `<iframe>` holds because screen readers have no title to read, so add a `title` describing its content.";
6984
6984
  const evaluateTitleValue = (value) => {
6985
6985
  if (!value) return "missing";
6986
6986
  if (isNodeOfType(value, "Literal")) {
@@ -7020,14 +7020,14 @@ const iframeHasTitle = defineRule({
7020
7020
  if (!titleAttr) {
7021
7021
  if (hasSpread || tag === "iframe") context.report({
7022
7022
  node: node.name,
7023
- message: MESSAGE$42
7023
+ message: MESSAGE$43
7024
7024
  });
7025
7025
  return;
7026
7026
  }
7027
7027
  const verdict = evaluateTitleValue(titleAttr.value);
7028
7028
  if (verdict === "missing" || verdict === "empty") context.report({
7029
7029
  node: titleAttr,
7030
- message: MESSAGE$42
7030
+ message: MESSAGE$43
7031
7031
  });
7032
7032
  } })
7033
7033
  });
@@ -7131,7 +7131,7 @@ const iframeMissingSandbox = defineRule({
7131
7131
  });
7132
7132
  //#endregion
7133
7133
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
7134
- const MESSAGE$41 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
7134
+ const MESSAGE$42 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
7135
7135
  const DEFAULT_COMPONENTS = ["img"];
7136
7136
  const DEFAULT_REDUNDANT_WORDS = [
7137
7137
  "image",
@@ -7194,7 +7194,7 @@ const imgRedundantAlt = defineRule({
7194
7194
  if (!altAttribute) return;
7195
7195
  if (altValueRedundant(altAttribute, settings.words)) context.report({
7196
7196
  node: altAttribute,
7197
- message: MESSAGE$41
7197
+ message: MESSAGE$42
7198
7198
  });
7199
7199
  } };
7200
7200
  }
@@ -9595,7 +9595,7 @@ const jsxMaxDepth = defineRule({
9595
9595
  });
9596
9596
  //#endregion
9597
9597
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
9598
- const MESSAGE$40 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
9598
+ const MESSAGE$41 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
9599
9599
  const LITERAL_TEXT_TAGS = new Set([
9600
9600
  "code",
9601
9601
  "pre",
@@ -9631,7 +9631,7 @@ const jsxNoCommentTextnodes = defineRule({
9631
9631
  if (isInsideLiteralTextTag(node)) return;
9632
9632
  context.report({
9633
9633
  node,
9634
- message: MESSAGE$40
9634
+ message: MESSAGE$41
9635
9635
  });
9636
9636
  } })
9637
9637
  });
@@ -9662,7 +9662,7 @@ const isInsideFunctionScope = (node) => {
9662
9662
  };
9663
9663
  //#endregion
9664
9664
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
9665
- const MESSAGE$39 = "Every reader of this context redraws on each render because you build its `value` inline.";
9665
+ const MESSAGE$40 = "Every reader of this context redraws on each render because you build its `value` inline.";
9666
9666
  const CONTEXT_MODULES$1 = [
9667
9667
  "react",
9668
9668
  "use-context-selector",
@@ -9760,7 +9760,7 @@ const jsxNoConstructedContextValues = defineRule({
9760
9760
  if (!isConstructedValue(innerExpression)) continue;
9761
9761
  context.report({
9762
9762
  node: attribute,
9763
- message: MESSAGE$39
9763
+ message: MESSAGE$40
9764
9764
  });
9765
9765
  }
9766
9766
  }
@@ -9846,7 +9846,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
9846
9846
  };
9847
9847
  //#endregion
9848
9848
  //#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
9849
- const MESSAGE$38 = "This child redraws every render because the prop gets brand new JSX each time.";
9849
+ const MESSAGE$39 = "This child redraws every render because the prop gets brand new JSX each time.";
9850
9850
  const KNOWN_SLOT_PROP_NAMES = new Set([
9851
9851
  "icon",
9852
9852
  "Icon",
@@ -10115,7 +10115,7 @@ const jsxNoJsxAsProp = defineRule({
10115
10115
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
10116
10116
  context.report({
10117
10117
  node,
10118
- message: MESSAGE$38
10118
+ message: MESSAGE$39
10119
10119
  });
10120
10120
  }
10121
10121
  };
@@ -10403,7 +10403,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
10403
10403
  ];
10404
10404
  //#endregion
10405
10405
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
10406
- const MESSAGE$37 = "This child redraws every render because the prop gets a brand new array each time.";
10406
+ const MESSAGE$38 = "This child redraws every render because the prop gets a brand new array each time.";
10407
10407
  const isDataArrayPropName = (propName) => {
10408
10408
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
10409
10409
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -10487,7 +10487,7 @@ const jsxNoNewArrayAsProp = defineRule({
10487
10487
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
10488
10488
  context.report({
10489
10489
  node,
10490
- message: MESSAGE$37
10490
+ message: MESSAGE$38
10491
10491
  });
10492
10492
  }
10493
10493
  };
@@ -10745,7 +10745,7 @@ const SAFE_RECEIVER_NAMES = new Set([
10745
10745
  ]);
10746
10746
  //#endregion
10747
10747
  //#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
10748
- const MESSAGE$36 = "This child redraws every render because the prop gets a brand new function each time.";
10748
+ const MESSAGE$37 = "This child redraws every render because the prop gets a brand new function each time.";
10749
10749
  const isAccessorPredicateName = (propName) => {
10750
10750
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
10751
10751
  if (propName.length <= prefix.length) continue;
@@ -10951,7 +10951,7 @@ const jsxNoNewFunctionAsProp = defineRule({
10951
10951
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
10952
10952
  context.report({
10953
10953
  node,
10954
- message: MESSAGE$36
10954
+ message: MESSAGE$37
10955
10955
  });
10956
10956
  }
10957
10957
  };
@@ -11171,7 +11171,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
11171
11171
  ];
11172
11172
  //#endregion
11173
11173
  //#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
11174
- const MESSAGE$35 = "This child redraws every render because the prop gets a brand new object each time.";
11174
+ const MESSAGE$36 = "This child redraws every render because the prop gets a brand new object each time.";
11175
11175
  const isConfigObjectPropName = (propName) => {
11176
11176
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
11177
11177
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11259,7 +11259,7 @@ const jsxNoNewObjectAsProp = defineRule({
11259
11259
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
11260
11260
  context.report({
11261
11261
  node,
11262
- message: MESSAGE$35
11262
+ message: MESSAGE$36
11263
11263
  });
11264
11264
  }
11265
11265
  };
@@ -11267,7 +11267,7 @@ const jsxNoNewObjectAsProp = defineRule({
11267
11267
  });
11268
11268
  //#endregion
11269
11269
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
11270
- const MESSAGE$34 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
11270
+ const MESSAGE$35 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
11271
11271
  const JAVASCRIPT_URL_PATTERN = /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;
11272
11272
  const resolveSettings$29 = (settings) => {
11273
11273
  const reactDoctor = settings?.["react-doctor"];
@@ -11308,7 +11308,7 @@ const jsxNoScriptUrl = defineRule({
11308
11308
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
11309
11309
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
11310
11310
  node: attribute,
11311
- message: MESSAGE$34
11311
+ message: MESSAGE$35
11312
11312
  });
11313
11313
  }
11314
11314
  } };
@@ -11904,7 +11904,7 @@ const jsxPropsNoSpreadMulti = defineRule({
11904
11904
  });
11905
11905
  //#endregion
11906
11906
  //#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
11907
- const MESSAGE$33 = "You can't tell what props reach this element when you spread them.";
11907
+ const MESSAGE$34 = "You can't tell what props reach this element when you spread them.";
11908
11908
  const resolveSettings$25 = (settings) => {
11909
11909
  const reactDoctor = settings?.["react-doctor"];
11910
11910
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -11945,7 +11945,7 @@ const jsxPropsNoSpreading = defineRule({
11945
11945
  }
11946
11946
  context.report({
11947
11947
  node: attribute,
11948
- message: MESSAGE$33
11948
+ message: MESSAGE$34
11949
11949
  });
11950
11950
  }
11951
11951
  } };
@@ -12101,7 +12101,7 @@ const labelHasAssociatedControl = defineRule({
12101
12101
  });
12102
12102
  //#endregion
12103
12103
  //#region src/plugin/rules/a11y/lang.ts
12104
- const MESSAGE$32 = "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`.";
12104
+ const MESSAGE$33 = "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`.";
12105
12105
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
12106
12106
  "aa",
12107
12107
  "ab",
@@ -12313,7 +12313,7 @@ const lang = defineRule({
12313
12313
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
12314
12314
  context.report({
12315
12315
  node: langAttr,
12316
- message: MESSAGE$32
12316
+ message: MESSAGE$33
12317
12317
  });
12318
12318
  return;
12319
12319
  }
@@ -12322,13 +12322,13 @@ const lang = defineRule({
12322
12322
  if (value === null) return;
12323
12323
  if (!isValidLangTag(value)) context.report({
12324
12324
  node: langAttr,
12325
- message: MESSAGE$32
12325
+ message: MESSAGE$33
12326
12326
  });
12327
12327
  } })
12328
12328
  });
12329
12329
  //#endregion
12330
12330
  //#region src/plugin/rules/a11y/media-has-caption.ts
12331
- const MESSAGE$31 = "Deaf & hard-of-hearing users miss this media without captions, so add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
12331
+ const MESSAGE$32 = "Deaf & hard-of-hearing users miss this media without captions, so add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
12332
12332
  const DEFAULT_AUDIO = ["audio"];
12333
12333
  const DEFAULT_VIDEO = ["video"];
12334
12334
  const DEFAULT_TRACK = ["track"];
@@ -12369,7 +12369,7 @@ const mediaHasCaption = defineRule({
12369
12369
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
12370
12370
  context.report({
12371
12371
  node: node.name,
12372
- message: MESSAGE$31
12372
+ message: MESSAGE$32
12373
12373
  });
12374
12374
  return;
12375
12375
  }
@@ -12386,7 +12386,7 @@ const mediaHasCaption = defineRule({
12386
12386
  return kindValue.value.toLowerCase() === "captions";
12387
12387
  })) context.report({
12388
12388
  node: node.name,
12389
- message: MESSAGE$31
12389
+ message: MESSAGE$32
12390
12390
  });
12391
12391
  } };
12392
12392
  }
@@ -12954,8 +12954,9 @@ const nextjsNoImgElement = defineRule({
12954
12954
  create: (context) => {
12955
12955
  const filename = normalizeFilename$1(context.filename ?? "");
12956
12956
  const isOgRoute = OG_ROUTE_PATTERN.test(filename);
12957
+ const isMetadataImageRoute = isNextjsMetadataImageRouteFilename(filename);
12957
12958
  return { JSXOpeningElement(node) {
12958
- if (isOgRoute) return;
12959
+ if (isOgRoute || isMetadataImageRoute) return;
12959
12960
  if (isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "img") context.report({
12960
12961
  node,
12961
12962
  message: "Plain <img> ships unoptimized, oversized images to your users."
@@ -13366,23 +13367,796 @@ const nextjsNoSideEffectInGetHandler = defineRule({
13366
13367
  }
13367
13368
  });
13368
13369
  //#endregion
13369
- //#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
13370
- const fileMentionsSuspense = (programNode) => {
13371
- let didSee = false;
13370
+ //#region src/plugin/utils/find-exported-function-body.ts
13371
+ const isFunctionLike = (node) => {
13372
+ if (!node) return false;
13373
+ return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
13374
+ };
13375
+ const findExportedFunctionBody = (programRoot, exportedName) => {
13376
+ if (!isNodeOfType(programRoot, "Program")) return null;
13377
+ const localBindings = /* @__PURE__ */ new Map();
13378
+ const namedExports = /* @__PURE__ */ new Map();
13379
+ let defaultExport = null;
13380
+ let defaultExportIdentifierName = null;
13381
+ const recordVariableDeclaration = (declaration) => {
13382
+ for (const declarator of declaration.declarations ?? []) {
13383
+ if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
13384
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
13385
+ const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
13386
+ if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
13387
+ }
13388
+ };
13389
+ for (const statement of programRoot.body ?? []) {
13390
+ if (isNodeOfType(statement, "VariableDeclaration")) {
13391
+ recordVariableDeclaration(statement);
13392
+ continue;
13393
+ }
13394
+ if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) {
13395
+ localBindings.set(statement.id.name, statement);
13396
+ continue;
13397
+ }
13398
+ if (isNodeOfType(statement, "ExportNamedDeclaration")) {
13399
+ const declaration = statement.declaration;
13400
+ if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
13401
+ recordVariableDeclaration(declaration);
13402
+ for (const declarator of declaration.declarations ?? []) {
13403
+ if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
13404
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
13405
+ namedExports.set(declarator.id.name, declarator.id.name);
13406
+ }
13407
+ } else if (declaration && isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
13408
+ localBindings.set(declaration.id.name, declaration);
13409
+ namedExports.set(declaration.id.name, declaration.id.name);
13410
+ }
13411
+ for (const specifier of statement.specifiers ?? []) {
13412
+ if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
13413
+ const local = specifier.local;
13414
+ const exported = specifier.exported;
13415
+ if (!isNodeOfType(local, "Identifier")) continue;
13416
+ const exportedNameSpec = isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null;
13417
+ if (!exportedNameSpec) continue;
13418
+ namedExports.set(exportedNameSpec, local.name);
13419
+ }
13420
+ continue;
13421
+ }
13422
+ if (isNodeOfType(statement, "ExportDefaultDeclaration")) {
13423
+ const declaration = statement.declaration;
13424
+ if (!declaration) continue;
13425
+ if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
13426
+ localBindings.set(declaration.id.name, declaration);
13427
+ defaultExport = declaration;
13428
+ continue;
13429
+ }
13430
+ if (isFunctionLike(declaration)) {
13431
+ defaultExport = declaration;
13432
+ continue;
13433
+ }
13434
+ if (isNodeOfType(declaration, "Identifier")) {
13435
+ defaultExportIdentifierName = declaration.name;
13436
+ continue;
13437
+ }
13438
+ }
13439
+ }
13440
+ if (exportedName === "default") {
13441
+ if (defaultExport) return defaultExport;
13442
+ if (defaultExportIdentifierName) {
13443
+ const binding = localBindings.get(defaultExportIdentifierName);
13444
+ if (binding) return binding;
13445
+ }
13446
+ }
13447
+ const localName = namedExports.get(exportedName);
13448
+ if (!localName) return null;
13449
+ return localBindings.get(localName) ?? null;
13450
+ };
13451
+ const resolveImportedExportName = (importSpecifier) => {
13452
+ if (isNodeOfType(importSpecifier, "ImportSpecifier")) {
13453
+ const imported = importSpecifier.imported;
13454
+ if (isNodeOfType(imported, "Identifier")) return imported.name;
13455
+ if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
13456
+ return null;
13457
+ }
13458
+ if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
13459
+ return null;
13460
+ };
13461
+ const findReExportSourcesForName = (programRoot, exportedName) => {
13462
+ if (!isNodeOfType(programRoot, "Program")) return [];
13463
+ const exportAllSources = [];
13464
+ for (const statement of programRoot.body ?? []) {
13465
+ if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
13466
+ const sourceValue = statement.source.value;
13467
+ if (typeof sourceValue !== "string") continue;
13468
+ for (const specifier of statement.specifiers ?? []) {
13469
+ if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
13470
+ const exported = specifier.exported;
13471
+ if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
13472
+ }
13473
+ }
13474
+ if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
13475
+ const sourceValue = statement.source.value;
13476
+ if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
13477
+ }
13478
+ }
13479
+ return exportAllSources;
13480
+ };
13481
+ //#endregion
13482
+ //#region src/plugin/utils/attach-parent-references.ts
13483
+ const attachParentReferences = (root) => {
13484
+ const visit = (node, parent) => {
13485
+ const writableNode = node;
13486
+ writableNode.parent = parent;
13487
+ const nodeRecord = node;
13488
+ for (const key of Object.keys(nodeRecord)) {
13489
+ if (key === "parent") continue;
13490
+ const child = nodeRecord[key];
13491
+ if (Array.isArray(child)) {
13492
+ for (const item of child) if (isAstNode(item)) visit(item, node);
13493
+ } else if (isAstNode(child)) visit(child, node);
13494
+ }
13495
+ };
13496
+ visit(root, null);
13497
+ };
13498
+ //#endregion
13499
+ //#region src/plugin/utils/parse-source-file.ts
13500
+ const FILENAME_TO_LANG = {
13501
+ ".ts": "ts",
13502
+ ".tsx": "tsx",
13503
+ ".js": "js",
13504
+ ".jsx": "jsx",
13505
+ ".mjs": "js",
13506
+ ".cjs": "js",
13507
+ ".mts": "ts",
13508
+ ".cts": "ts"
13509
+ };
13510
+ const resolveLang = (filename) => {
13511
+ return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
13512
+ };
13513
+ const parseCache = /* @__PURE__ */ new Map();
13514
+ const parseSourceFile = (absoluteFilePath) => {
13515
+ let fileStat;
13516
+ try {
13517
+ fileStat = fs.statSync(absoluteFilePath);
13518
+ } catch {
13519
+ return null;
13520
+ }
13521
+ if (!fileStat.isFile()) return null;
13522
+ if (fileStat.size > 2e6) return null;
13523
+ const cached = parseCache.get(absoluteFilePath);
13524
+ if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
13525
+ if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
13526
+ parseCache.set(absoluteFilePath, {
13527
+ mtimeMs: fileStat.mtimeMs,
13528
+ size: fileStat.size,
13529
+ program: null
13530
+ });
13531
+ return null;
13532
+ }
13533
+ let sourceText;
13534
+ try {
13535
+ sourceText = fs.readFileSync(absoluteFilePath, "utf8");
13536
+ } catch {
13537
+ parseCache.set(absoluteFilePath, {
13538
+ mtimeMs: fileStat.mtimeMs,
13539
+ size: fileStat.size,
13540
+ program: null
13541
+ });
13542
+ return null;
13543
+ }
13544
+ let parsedProgram = null;
13545
+ try {
13546
+ const result = parseSync(absoluteFilePath, sourceText, {
13547
+ astType: "ts",
13548
+ lang: resolveLang(absoluteFilePath)
13549
+ });
13550
+ if (!result.errors.some((parseError) => parseError.severity === "Error")) {
13551
+ parsedProgram = result.program;
13552
+ attachParentReferences(parsedProgram);
13553
+ }
13554
+ } catch {
13555
+ parsedProgram = null;
13556
+ }
13557
+ parseCache.set(absoluteFilePath, {
13558
+ mtimeMs: fileStat.mtimeMs,
13559
+ size: fileStat.size,
13560
+ program: parsedProgram
13561
+ });
13562
+ return parsedProgram;
13563
+ };
13564
+ //#endregion
13565
+ //#region src/plugin/utils/parse-export-specifiers.ts
13566
+ const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
13567
+ const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
13568
+ const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
13569
+ const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
13570
+ const localName = getSpecifierName(rawLocalName ?? "");
13571
+ return {
13572
+ localName,
13573
+ exportedName: getSpecifierName(rawExportedName ?? localName),
13574
+ isTypeOnly
13575
+ };
13576
+ });
13577
+ //#endregion
13578
+ //#region src/plugin/utils/strip-js-comments.ts
13579
+ const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
13580
+ const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
13581
+ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
13582
+ //#endregion
13583
+ //#region src/plugin/utils/does-module-export-name.ts
13584
+ const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
13585
+ const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
13586
+ const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
13587
+ const doesSourceTextExportName = (sourceText, exportedName) => {
13588
+ const strippedSource = stripJsComments(sourceText);
13589
+ if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
13590
+ for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
13591
+ for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
13592
+ return false;
13593
+ };
13594
+ const doesModuleExportName = (filePath, exportedName) => {
13595
+ try {
13596
+ return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
13597
+ } catch {
13598
+ return false;
13599
+ }
13600
+ };
13601
+ //#endregion
13602
+ //#region src/plugin/utils/is-barrel-index-module.ts
13603
+ const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
13604
+ const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
13605
+ const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
13606
+ const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
13607
+ const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
13608
+ const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
13609
+ const createNonBarrelInfo = () => ({
13610
+ isBarrel: false,
13611
+ exportsByName: /* @__PURE__ */ new Map(),
13612
+ starExportSources: []
13613
+ });
13614
+ const addImportedBinding = (importedBindings, binding) => {
13615
+ importedBindings.set(binding.localName, {
13616
+ ...binding,
13617
+ didExport: false
13618
+ });
13619
+ };
13620
+ const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
13621
+ for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
13622
+ localName: specifier.exportedName,
13623
+ importedName: specifier.localName,
13624
+ source,
13625
+ isTypeOnly: specifier.isTypeOnly
13626
+ });
13627
+ };
13628
+ const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
13629
+ const trimmedImportClause = importClause.trim();
13630
+ const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
13631
+ if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
13632
+ localName: namespaceMatch[1],
13633
+ importedName: "*",
13634
+ source,
13635
+ isTypeOnly: declarationIsTypeOnly
13636
+ });
13637
+ const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
13638
+ if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
13639
+ const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
13640
+ if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
13641
+ localName: defaultImportName,
13642
+ importedName: "default",
13643
+ source,
13644
+ isTypeOnly: declarationIsTypeOnly
13645
+ });
13646
+ };
13647
+ const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
13648
+ let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
13649
+ collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
13650
+ return "";
13651
+ });
13652
+ withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
13653
+ const isTypeOnly = Boolean(typeKeyword);
13654
+ if (namespaceExportName) {
13655
+ exportsByName.set(namespaceExportName, {
13656
+ exportedName: namespaceExportName,
13657
+ importedName: "*",
13658
+ source,
13659
+ isTypeOnly
13660
+ });
13661
+ return "";
13662
+ }
13663
+ if (specifiersText) {
13664
+ for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
13665
+ exportedName: specifier.exportedName,
13666
+ importedName: specifier.localName,
13667
+ source,
13668
+ isTypeOnly: specifier.isTypeOnly
13669
+ });
13670
+ return "";
13671
+ }
13672
+ starExportSources.push(source);
13673
+ return "";
13674
+ });
13675
+ withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
13676
+ for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
13677
+ const importedBinding = importedBindings.get(specifier.localName);
13678
+ if (!importedBinding) return _match;
13679
+ importedBinding.didExport = true;
13680
+ exportsByName.set(specifier.exportedName, {
13681
+ exportedName: specifier.exportedName,
13682
+ importedName: importedBinding.importedName,
13683
+ source: importedBinding.source,
13684
+ isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
13685
+ });
13686
+ }
13687
+ return "";
13688
+ });
13689
+ return withoutKnownDeclarations;
13690
+ };
13691
+ const hasUnexportedRuntimeImport = (importedBindings) => {
13692
+ for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
13693
+ return false;
13694
+ };
13695
+ const classifyBarrelModule = (sourceText) => {
13696
+ const strippedSource = stripJsComments(sourceText).trim();
13697
+ if (!strippedSource) return createNonBarrelInfo();
13698
+ const importedBindings = /* @__PURE__ */ new Map();
13699
+ const exportsByName = /* @__PURE__ */ new Map();
13700
+ const starExportSources = [];
13701
+ if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
13702
+ return {
13703
+ isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
13704
+ exportsByName,
13705
+ starExportSources
13706
+ };
13707
+ };
13708
+ const getBarrelIndexModuleInfo = (filePath) => {
13709
+ if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
13710
+ const cachedResult = barrelIndexModuleInfoCache.get(filePath);
13711
+ if (cachedResult !== void 0) return cachedResult;
13712
+ let moduleInfo = createNonBarrelInfo();
13713
+ try {
13714
+ moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
13715
+ } catch {
13716
+ moduleInfo = createNonBarrelInfo();
13717
+ }
13718
+ barrelIndexModuleInfoCache.set(filePath, moduleInfo);
13719
+ return moduleInfo;
13720
+ };
13721
+ const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
13722
+ //#endregion
13723
+ //#region src/plugin/utils/resolve-relative-import-path.ts
13724
+ const MODULE_FILE_EXTENSIONS = [
13725
+ ".ts",
13726
+ ".tsx",
13727
+ ".js",
13728
+ ".jsx",
13729
+ ".mjs",
13730
+ ".cjs",
13731
+ ".mts",
13732
+ ".cts"
13733
+ ];
13734
+ const PACKAGE_EXPORT_CONDITIONS = [
13735
+ "import",
13736
+ "default",
13737
+ "module",
13738
+ "browser",
13739
+ "require"
13740
+ ];
13741
+ const PACKAGE_ENTRY_FIELDS = [
13742
+ "module",
13743
+ "main",
13744
+ "browser"
13745
+ ];
13746
+ const getExistingFilePath = (filePath) => {
13747
+ try {
13748
+ return fs.statSync(filePath).isFile() ? filePath : null;
13749
+ } catch {
13750
+ return null;
13751
+ }
13752
+ };
13753
+ const getExistingDirectoryPath = (directoryPath) => {
13754
+ try {
13755
+ return fs.statSync(directoryPath).isDirectory() ? directoryPath : null;
13756
+ } catch {
13757
+ return null;
13758
+ }
13759
+ };
13760
+ const getModuleFilePathCandidates = (modulePath) => {
13761
+ const extension = path.extname(modulePath);
13762
+ if (!extension) return MODULE_FILE_EXTENSIONS.map((moduleExtension) => `${modulePath}${moduleExtension}`);
13763
+ const modulePathWithoutExtension = modulePath.slice(0, -extension.length);
13764
+ if (extension === ".js") return [
13765
+ modulePath,
13766
+ `${modulePathWithoutExtension}.ts`,
13767
+ `${modulePathWithoutExtension}.tsx`,
13768
+ `${modulePathWithoutExtension}.jsx`
13769
+ ];
13770
+ if (extension === ".jsx") return [modulePath, `${modulePathWithoutExtension}.tsx`];
13771
+ if (extension === ".mjs") return [modulePath, `${modulePathWithoutExtension}.mts`];
13772
+ if (extension === ".cjs") return [modulePath, `${modulePathWithoutExtension}.cts`];
13773
+ return [modulePath];
13774
+ };
13775
+ const isObjectRecord$1 = (value) => typeof value === "object" && value !== null;
13776
+ const getConditionalExportEntry = (exportEntry) => {
13777
+ if (typeof exportEntry === "string") return exportEntry;
13778
+ if (Array.isArray(exportEntry)) {
13779
+ for (const fallbackEntry of exportEntry) {
13780
+ const resolvedFallbackEntry = getConditionalExportEntry(fallbackEntry);
13781
+ if (resolvedFallbackEntry) return resolvedFallbackEntry;
13782
+ }
13783
+ return null;
13784
+ }
13785
+ if (!isObjectRecord$1(exportEntry)) return null;
13786
+ for (const condition of PACKAGE_EXPORT_CONDITIONS) {
13787
+ const nestedEntry = getConditionalExportEntry(exportEntry[condition]);
13788
+ if (nestedEntry) return nestedEntry;
13789
+ }
13790
+ return null;
13791
+ };
13792
+ const getPackageExportEntry = (packageJson) => {
13793
+ const exportsField = packageJson.exports;
13794
+ if (!exportsField) return null;
13795
+ const directExportEntry = getConditionalExportEntry(exportsField);
13796
+ if (directExportEntry) return directExportEntry;
13797
+ if (!isObjectRecord$1(exportsField)) return null;
13798
+ return getConditionalExportEntry(exportsField["."]);
13799
+ };
13800
+ const resolveModulePathWithIndexFallback = (modulePath) => {
13801
+ const filePath = resolveModuleFilePath(modulePath);
13802
+ if (filePath) return filePath;
13803
+ return resolveModuleFilePath(path.join(modulePath, "index"));
13804
+ };
13805
+ const resolvePackageDirectoryEntry = (directoryPath) => {
13806
+ const existingDirectoryPath = getExistingDirectoryPath(directoryPath);
13807
+ if (!existingDirectoryPath) return null;
13808
+ const packageJsonPath = path.join(existingDirectoryPath, "package.json");
13809
+ try {
13810
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
13811
+ const packageEntry = getPackageExportEntry(packageJson) ?? PACKAGE_ENTRY_FIELDS.map((fieldName) => packageJson[fieldName]).find((value) => typeof value === "string");
13812
+ if (!packageEntry) return null;
13813
+ return resolveModulePathWithIndexFallback(path.resolve(existingDirectoryPath, packageEntry));
13814
+ } catch {
13815
+ return null;
13816
+ }
13817
+ };
13818
+ const resolveModuleFilePath = (modulePath) => {
13819
+ const exactFilePath = getExistingFilePath(modulePath);
13820
+ if (exactFilePath) return exactFilePath;
13821
+ for (const candidateFilePath of getModuleFilePathCandidates(modulePath)) {
13822
+ const filePath = getExistingFilePath(candidateFilePath);
13823
+ if (filePath) return filePath;
13824
+ }
13825
+ return null;
13826
+ };
13827
+ const resolveModuleFileFromAbsolutePath = (importPath) => {
13828
+ const directFilePath = resolveModuleFilePath(importPath);
13829
+ if (directFilePath) return directFilePath;
13830
+ const packageEntryFilePath = resolvePackageDirectoryEntry(importPath);
13831
+ if (packageEntryFilePath) return packageEntryFilePath;
13832
+ return resolveModuleFilePath(path.join(importPath, "index"));
13833
+ };
13834
+ const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
13835
+ //#endregion
13836
+ //#region src/plugin/utils/resolve-barrel-export-file-path.ts
13837
+ const getUniqueFilePath = (filePaths) => {
13838
+ const uniqueFilePaths = new Set(filePaths);
13839
+ if (uniqueFilePaths.size !== 1) return null;
13840
+ const [filePath] = uniqueFilePaths;
13841
+ return filePath ?? null;
13842
+ };
13843
+ const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
13844
+ const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
13845
+ if (!resolvedTargetPath) return null;
13846
+ const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
13847
+ if (nestedTargetPath) return nestedTargetPath;
13848
+ return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
13849
+ };
13850
+ const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
13851
+ if (visitedFilePaths.has(barrelFilePath)) return null;
13852
+ visitedFilePaths.add(barrelFilePath);
13853
+ const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
13854
+ if (!moduleInfo.isBarrel) return null;
13855
+ const target = moduleInfo.exportsByName.get(exportedName);
13856
+ if (target) {
13857
+ const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
13858
+ if (!resolvedTargetPath) return null;
13859
+ return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
13860
+ }
13861
+ if (exportedName === "default") return null;
13862
+ return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
13863
+ };
13864
+ //#endregion
13865
+ //#region src/plugin/utils/resolve-tsconfig-alias.ts
13866
+ const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
13867
+ const isObjectRecord = (value) => typeof value === "object" && value !== null;
13868
+ const stripJsonComments = (text) => {
13869
+ let output = "";
13870
+ let inString = false;
13871
+ let inLineComment = false;
13872
+ let inBlockComment = false;
13873
+ for (let index = 0; index < text.length; index++) {
13874
+ const character = text[index];
13875
+ const nextCharacter = text[index + 1];
13876
+ if (inLineComment) {
13877
+ if (character === "\n") {
13878
+ inLineComment = false;
13879
+ output += character;
13880
+ }
13881
+ continue;
13882
+ }
13883
+ if (inBlockComment) {
13884
+ if (character === "*" && nextCharacter === "/") {
13885
+ inBlockComment = false;
13886
+ index++;
13887
+ }
13888
+ continue;
13889
+ }
13890
+ if (inString) {
13891
+ output += character;
13892
+ if (character === "\\") {
13893
+ output += nextCharacter ?? "";
13894
+ index++;
13895
+ } else if (character === "\"") inString = false;
13896
+ continue;
13897
+ }
13898
+ if (character === "\"") {
13899
+ inString = true;
13900
+ output += character;
13901
+ continue;
13902
+ }
13903
+ if (character === "/" && nextCharacter === "/") {
13904
+ inLineComment = true;
13905
+ index++;
13906
+ continue;
13907
+ }
13908
+ if (character === "/" && nextCharacter === "*") {
13909
+ inBlockComment = true;
13910
+ index++;
13911
+ continue;
13912
+ }
13913
+ output += character;
13914
+ }
13915
+ return output.replace(/,(\s*[}\]])/g, "$1");
13916
+ };
13917
+ const parseTsconfigFile = (configFilePath) => {
13918
+ let sourceText;
13919
+ try {
13920
+ sourceText = fs.readFileSync(configFilePath, "utf8");
13921
+ } catch {
13922
+ return null;
13923
+ }
13924
+ try {
13925
+ const parsed = JSON.parse(stripJsonComments(sourceText));
13926
+ return isObjectRecord(parsed) ? parsed : null;
13927
+ } catch {
13928
+ return null;
13929
+ }
13930
+ };
13931
+ const resolveExtendsPath = (extendsValue, fromConfigDirectory) => {
13932
+ const withExtension = extendsValue.endsWith(".json") ? extendsValue : `${extendsValue}.json`;
13933
+ if (extendsValue.startsWith("./") || extendsValue.startsWith("../")) return path.resolve(fromConfigDirectory, withExtension);
13934
+ return path.join(fromConfigDirectory, "node_modules", withExtension);
13935
+ };
13936
+ const parsePathsField = (pathsField) => {
13937
+ const paths = /* @__PURE__ */ new Map();
13938
+ if (!isObjectRecord(pathsField)) return paths;
13939
+ for (const [pattern, targets] of Object.entries(pathsField)) {
13940
+ if (!Array.isArray(targets)) continue;
13941
+ const stringTargets = targets.filter((target) => typeof target === "string");
13942
+ if (stringTargets.length > 0) paths.set(pattern, stringTargets);
13943
+ }
13944
+ return paths;
13945
+ };
13946
+ const readResolvedTsconfig = (configFilePath, extendsDepth) => {
13947
+ const parsed = parseTsconfigFile(configFilePath);
13948
+ if (!parsed) return null;
13949
+ const configDirectory = path.dirname(configFilePath);
13950
+ const compilerOptions = isObjectRecord(parsed.compilerOptions) ? parsed.compilerOptions : {};
13951
+ const baseUrlValue = typeof compilerOptions.baseUrl === "string" ? compilerOptions.baseUrl : null;
13952
+ const hasExplicitBaseUrl = baseUrlValue !== null;
13953
+ const baseAbsolutePath = baseUrlValue !== null ? path.resolve(configDirectory, baseUrlValue) : configDirectory;
13954
+ if (isObjectRecord(compilerOptions.paths)) return {
13955
+ baseAbsolutePath,
13956
+ hasExplicitBaseUrl,
13957
+ paths: parsePathsField(compilerOptions.paths)
13958
+ };
13959
+ if (typeof parsed.extends === "string" && extendsDepth < 8) {
13960
+ const parentPath = resolveExtendsPath(parsed.extends, configDirectory);
13961
+ const inherited = parentPath ? readResolvedTsconfig(parentPath, extendsDepth + 1) : null;
13962
+ if (inherited) return inherited;
13963
+ }
13964
+ return hasExplicitBaseUrl ? {
13965
+ baseAbsolutePath,
13966
+ hasExplicitBaseUrl,
13967
+ paths: /* @__PURE__ */ new Map()
13968
+ } : null;
13969
+ };
13970
+ const configByFilePath = /* @__PURE__ */ new Map();
13971
+ const loadTsconfigCached = (configFilePath) => {
13972
+ let fileStat;
13973
+ try {
13974
+ fileStat = fs.statSync(configFilePath);
13975
+ } catch {
13976
+ return null;
13977
+ }
13978
+ const cached = configByFilePath.get(configFilePath);
13979
+ if (cached && cached.mtimeMs === fileStat.mtimeMs) return cached.config;
13980
+ const config = readResolvedTsconfig(configFilePath, 0);
13981
+ configByFilePath.set(configFilePath, {
13982
+ mtimeMs: fileStat.mtimeMs,
13983
+ config
13984
+ });
13985
+ return config;
13986
+ };
13987
+ const findNearestTsconfig = (fromDirectory) => {
13988
+ let currentDirectory = fromDirectory;
13989
+ for (let level = 0; level < 30; level++) {
13990
+ for (const fileName of TSCONFIG_FILE_NAMES) {
13991
+ const candidate = loadTsconfigCached(path.join(currentDirectory, fileName));
13992
+ if (candidate) return candidate;
13993
+ }
13994
+ const parentDirectory = path.dirname(currentDirectory);
13995
+ if (parentDirectory === currentDirectory) break;
13996
+ currentDirectory = parentDirectory;
13997
+ }
13998
+ return null;
13999
+ };
14000
+ const matchPathPattern = (source, pattern) => {
14001
+ const starIndex = pattern.indexOf("*");
14002
+ if (starIndex === -1) return source === pattern ? "" : null;
14003
+ const prefix = pattern.slice(0, starIndex);
14004
+ const suffix = pattern.slice(starIndex + 1);
14005
+ if (source.length >= prefix.length + suffix.length && source.startsWith(prefix) && source.endsWith(suffix)) return source.slice(prefix.length, source.length - suffix.length);
14006
+ return null;
14007
+ };
14008
+ const resolveTsconfigAliasPath = (fromFilename, source) => {
14009
+ const config = findNearestTsconfig(path.dirname(fromFilename));
14010
+ if (!config) return null;
14011
+ let bestPattern = null;
14012
+ let bestCapture = "";
14013
+ let bestPrefixLength = -1;
14014
+ for (const pattern of config.paths.keys()) {
14015
+ const capture = matchPathPattern(source, pattern);
14016
+ if (capture === null) continue;
14017
+ const starIndex = pattern.indexOf("*");
14018
+ const prefixLength = starIndex === -1 ? pattern.length : starIndex;
14019
+ if (prefixLength > bestPrefixLength) {
14020
+ bestPattern = pattern;
14021
+ bestCapture = capture;
14022
+ bestPrefixLength = prefixLength;
14023
+ }
14024
+ }
14025
+ if (bestPattern) for (const target of config.paths.get(bestPattern) ?? []) {
14026
+ const substituted = target.replaceAll("*", bestCapture);
14027
+ const resolved = resolveModuleFileFromAbsolutePath(path.resolve(config.baseAbsolutePath, substituted));
14028
+ if (resolved) return resolved;
14029
+ }
14030
+ if (config.hasExplicitBaseUrl) return resolveModuleFileFromAbsolutePath(path.resolve(config.baseAbsolutePath, source));
14031
+ return null;
14032
+ };
14033
+ //#endregion
14034
+ //#region src/plugin/utils/resolve-module-path.ts
14035
+ const resolveModulePath = (fromFilename, source) => resolveRelativeImportPath(fromFilename, source) ?? resolveTsconfigAliasPath(fromFilename, source);
14036
+ //#endregion
14037
+ //#region src/plugin/utils/resolve-cross-file-function-export.ts
14038
+ const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
14039
+ if (visitedFilePaths.size >= 4) return null;
14040
+ if (visitedFilePaths.has(filePath)) return null;
14041
+ visitedFilePaths.add(filePath);
14042
+ const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
14043
+ const programRoot = parseSourceFile(actualFilePath);
14044
+ if (!programRoot) return null;
14045
+ const exported = findExportedFunctionBody(programRoot, exportedName);
14046
+ if (exported) return exported;
14047
+ for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
14048
+ const nextFilePath = resolveModulePath(actualFilePath, reExportSource);
14049
+ if (!nextFilePath) continue;
14050
+ const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
14051
+ if (resolved) return resolved;
14052
+ }
14053
+ return null;
14054
+ };
14055
+ const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
14056
+ const resolvedFilePath = resolveModulePath(fromFilename, source);
14057
+ if (!resolvedFilePath) return null;
14058
+ return resolveFunctionExportInFile(resolvedFilePath, exportedName, /* @__PURE__ */ new Set());
14059
+ };
14060
+ //#endregion
14061
+ //#region src/plugin/utils/ast-mentions-suspense.ts
14062
+ const isSuspenseJsxOpeningName = (name) => {
14063
+ if (isNodeOfType(name, "JSXIdentifier")) return name.name === "Suspense";
14064
+ return isNodeOfType(name, "JSXMemberExpression") && isNodeOfType(name.property, "JSXIdentifier") && name.property.name === "Suspense";
14065
+ };
14066
+ const astMentionsSuspense = (programNode) => {
14067
+ let didDetect = false;
13372
14068
  walkAst(programNode, (child) => {
13373
- if (didSee) return false;
13374
- if (isNodeOfType(child, "JSXOpeningElement") && isNodeOfType(child.name, "JSXIdentifier") && child.name.name === "Suspense") {
13375
- didSee = true;
14069
+ if (didDetect) return false;
14070
+ if (isNodeOfType(child, "JSXOpeningElement") && isSuspenseJsxOpeningName(child.name)) {
14071
+ didDetect = true;
13376
14072
  return false;
13377
14073
  }
13378
14074
  if (isNodeOfType(child, "ImportDeclaration") && child.source?.value === "react") {
13379
14075
  if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense")) {
13380
- didSee = true;
14076
+ didDetect = true;
13381
14077
  return false;
13382
14078
  }
13383
14079
  }
13384
14080
  });
13385
- return didSee;
14081
+ return didDetect;
14082
+ };
14083
+ //#endregion
14084
+ //#region src/plugin/utils/find-ancestor-suspense-layout.ts
14085
+ const LAYOUT_FILE_NAMES = [
14086
+ "layout.tsx",
14087
+ "layout.jsx",
14088
+ "layout.ts",
14089
+ "layout.js"
14090
+ ];
14091
+ const hasAncestorSuspenseLayout = (pageFilename) => {
14092
+ const normalizedPage = pageFilename.replaceAll("\\", "/");
14093
+ let currentDirectory = path.dirname(normalizedPage);
14094
+ for (let level = 0; level < 30; level++) {
14095
+ for (const layoutFileName of LAYOUT_FILE_NAMES) {
14096
+ const layoutPath = path.join(currentDirectory, layoutFileName);
14097
+ if (layoutPath.replaceAll("\\", "/") === normalizedPage) continue;
14098
+ const programRoot = parseSourceFile(layoutPath);
14099
+ if (programRoot && astMentionsSuspense(programRoot)) return true;
14100
+ }
14101
+ if (path.basename(currentDirectory) === "app") break;
14102
+ const parentDirectory = path.dirname(currentDirectory);
14103
+ if (parentDirectory === currentDirectory) break;
14104
+ currentDirectory = parentDirectory;
14105
+ }
14106
+ return false;
14107
+ };
14108
+ //#endregion
14109
+ //#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
14110
+ const astContainsUseSearchParams = (root) => {
14111
+ let didFind = false;
14112
+ walkAst(root, (child) => {
14113
+ if (didFind) return false;
14114
+ if (isHookCall$1(child, "useSearchParams")) {
14115
+ didFind = true;
14116
+ return false;
14117
+ }
14118
+ });
14119
+ return didFind;
14120
+ };
14121
+ const isSuspenseJsxName = (name, suspenseLocalNames) => {
14122
+ if (isNodeOfType(name, "JSXIdentifier")) return name.name === "Suspense" || suspenseLocalNames.has(name.name);
14123
+ return isNodeOfType(name, "JSXMemberExpression") && isNodeOfType(name.property, "JSXIdentifier") && name.property.name === "Suspense";
14124
+ };
14125
+ const isInsideSuspenseBoundary = (node, suspenseLocalNames) => {
14126
+ let ancestor = node.parent;
14127
+ while (ancestor) {
14128
+ if (isNodeOfType(ancestor, "JSXElement") && isSuspenseJsxName(ancestor.openingElement?.name, suspenseLocalNames)) return true;
14129
+ ancestor = ancestor.parent ?? null;
14130
+ }
14131
+ return false;
14132
+ };
14133
+ const collectSuspenseLocalNames = (programNode) => {
14134
+ const names = /* @__PURE__ */ new Set();
14135
+ for (const statement of programNode.body ?? []) {
14136
+ if (!isNodeOfType(statement, "ImportDeclaration")) continue;
14137
+ if (statement.source?.value !== "react") continue;
14138
+ for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense" && specifier.local?.name) names.add(specifier.local.name);
14139
+ }
14140
+ return names;
14141
+ };
14142
+ const collectImportedComponents = (programNode) => {
14143
+ const entries = /* @__PURE__ */ new Map();
14144
+ for (const statement of programNode.body ?? []) {
14145
+ if (!isNodeOfType(statement, "ImportDeclaration")) continue;
14146
+ if (typeof statement.source?.value !== "string") continue;
14147
+ const source = statement.source.value;
14148
+ for (const specifier of statement.specifiers ?? []) {
14149
+ const localName = specifier.local?.name;
14150
+ if (!localName) continue;
14151
+ const exportedName = resolveImportedExportName(specifier);
14152
+ if (!exportedName) continue;
14153
+ entries.set(localName, {
14154
+ source,
14155
+ exportedName
14156
+ });
14157
+ }
14158
+ }
14159
+ return entries;
13386
14160
  };
13387
14161
  const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
13388
14162
  id: "nextjs-no-use-search-params-without-suspense",
@@ -13392,18 +14166,44 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
13392
14166
  severity: "warn",
13393
14167
  recommendation: "Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`",
13394
14168
  create: (context) => {
14169
+ let isPageOrLayoutFile = false;
14170
+ let hasAncestorLayoutSuspense = false;
13395
14171
  let hasSuspenseInFile = false;
14172
+ let importedComponents = /* @__PURE__ */ new Map();
14173
+ let suspenseLocalNames = /* @__PURE__ */ new Set();
13396
14174
  return {
13397
14175
  Program(programNode) {
13398
- hasSuspenseInFile = fileMentionsSuspense(programNode);
14176
+ const filename = normalizeFilename$1(context.filename ?? "");
14177
+ isPageOrLayoutFile = PAGE_OR_LAYOUT_FILE_PATTERN.test(filename);
14178
+ if (!isPageOrLayoutFile) return;
14179
+ hasAncestorLayoutSuspense = hasAncestorSuspenseLayout(context.filename ?? "");
14180
+ if (hasAncestorLayoutSuspense) return;
14181
+ hasSuspenseInFile = astMentionsSuspense(programNode);
14182
+ importedComponents = collectImportedComponents(programNode);
14183
+ suspenseLocalNames = collectSuspenseLocalNames(programNode);
13399
14184
  },
13400
14185
  CallExpression(node) {
13401
- if (hasSuspenseInFile) return;
14186
+ if (!isPageOrLayoutFile || hasAncestorLayoutSuspense || hasSuspenseInFile) return;
13402
14187
  if (!isHookCall$1(node, "useSearchParams")) return;
13403
14188
  context.report({
13404
14189
  node,
13405
14190
  message: "useSearchParams() without a <Suspense> boundary forces the whole page into client-side rendering."
13406
14191
  });
14192
+ },
14193
+ JSXOpeningElement(node) {
14194
+ if (!isPageOrLayoutFile || hasAncestorLayoutSuspense) return;
14195
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
14196
+ const importEntry = importedComponents.get(node.name.name);
14197
+ if (!importEntry) return;
14198
+ const jsxElement = node.parent;
14199
+ if (!jsxElement) return;
14200
+ if (isInsideSuspenseBoundary(jsxElement, suspenseLocalNames)) return;
14201
+ const componentBody = resolveCrossFileFunctionExport(context.filename ?? "", importEntry.source, importEntry.exportedName);
14202
+ if (!componentBody || !astContainsUseSearchParams(componentBody)) return;
14203
+ context.report({
14204
+ node,
14205
+ message: `<${node.name.name}> uses useSearchParams() but is not wrapped in a <Suspense> boundary.`
14206
+ });
13407
14207
  }
13408
14208
  };
13409
14209
  }
@@ -13427,7 +14227,7 @@ const nextjsNoVercelOgImport = defineRule({
13427
14227
  });
13428
14228
  //#endregion
13429
14229
  //#region src/plugin/rules/a11y/no-access-key.ts
13430
- const MESSAGE$30 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
14230
+ const MESSAGE$31 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
13431
14231
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
13432
14232
  const noAccessKey = defineRule({
13433
14233
  id: "no-access-key",
@@ -13444,7 +14244,7 @@ const noAccessKey = defineRule({
13444
14244
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
13445
14245
  context.report({
13446
14246
  node: accessKey,
13447
- message: MESSAGE$30
14247
+ message: MESSAGE$31
13448
14248
  });
13449
14249
  return;
13450
14250
  }
@@ -13454,7 +14254,7 @@ const noAccessKey = defineRule({
13454
14254
  if (isUndefinedIdentifier(expression)) return;
13455
14255
  context.report({
13456
14256
  node: accessKey,
13457
- message: MESSAGE$30
14257
+ message: MESSAGE$31
13458
14258
  });
13459
14259
  }
13460
14260
  } })
@@ -13937,7 +14737,7 @@ const noAdjustStateOnPropChange = defineRule({
13937
14737
  });
13938
14738
  //#endregion
13939
14739
  //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
13940
- const MESSAGE$29 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
14740
+ const MESSAGE$30 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
13941
14741
  const noAriaHiddenOnFocusable = defineRule({
13942
14742
  id: "no-aria-hidden-on-focusable",
13943
14743
  title: "aria-hidden on focusable element",
@@ -13964,7 +14764,7 @@ const noAriaHiddenOnFocusable = defineRule({
13964
14764
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
13965
14765
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
13966
14766
  node: ariaHidden,
13967
- message: MESSAGE$29
14767
+ message: MESSAGE$30
13968
14768
  });
13969
14769
  } })
13970
14770
  });
@@ -14332,7 +15132,7 @@ const noArrayIndexAsKey = defineRule({
14332
15132
  });
14333
15133
  //#endregion
14334
15134
  //#region src/plugin/rules/react-builtins/no-array-index-key.ts
14335
- const MESSAGE$28 = "Your users can see & submit the wrong data when this list reorders.";
15135
+ const MESSAGE$29 = "Your users can see & submit the wrong data when this list reorders.";
14336
15136
  const SECOND_INDEX_METHODS = new Set([
14337
15137
  "every",
14338
15138
  "filter",
@@ -14536,7 +15336,7 @@ const noArrayIndexKey = defineRule({
14536
15336
  }
14537
15337
  context.report({
14538
15338
  node: keyAttribute,
14539
- message: MESSAGE$28
15339
+ message: MESSAGE$29
14540
15340
  });
14541
15341
  },
14542
15342
  CallExpression(node) {
@@ -14556,7 +15356,7 @@ const noArrayIndexKey = defineRule({
14556
15356
  if (propName !== "key") continue;
14557
15357
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
14558
15358
  node: property,
14559
- message: MESSAGE$28
15359
+ message: MESSAGE$29
14560
15360
  });
14561
15361
  }
14562
15362
  }
@@ -14564,7 +15364,7 @@ const noArrayIndexKey = defineRule({
14564
15364
  });
14565
15365
  //#endregion
14566
15366
  //#region src/plugin/rules/a11y/no-autofocus.ts
14567
- const MESSAGE$27 = "Screen reader & keyboard users get disoriented because `autoFocus` jumps focus on load, so remove it and let people choose where to focus.";
15367
+ const MESSAGE$28 = "Screen reader & keyboard users get disoriented because `autoFocus` jumps focus on load, so remove it and let people choose where to focus.";
14568
15368
  const resolveSettings$21 = (settings) => {
14569
15369
  const reactDoctor = settings?.["react-doctor"];
14570
15370
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -14620,7 +15420,7 @@ const noAutofocus = defineRule({
14620
15420
  }
14621
15421
  context.report({
14622
15422
  node: autoFocusAttribute,
14623
- message: MESSAGE$27
15423
+ message: MESSAGE$28
14624
15424
  });
14625
15425
  } };
14626
15426
  }
@@ -14634,306 +15434,6 @@ const createRelativeImportSource = (filename, targetFilePath) => {
14634
15434
  return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
14635
15435
  };
14636
15436
  //#endregion
14637
- //#region src/plugin/utils/parse-export-specifiers.ts
14638
- const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
14639
- const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
14640
- const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
14641
- const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
14642
- const localName = getSpecifierName(rawLocalName ?? "");
14643
- return {
14644
- localName,
14645
- exportedName: getSpecifierName(rawExportedName ?? localName),
14646
- isTypeOnly
14647
- };
14648
- });
14649
- //#endregion
14650
- //#region src/plugin/utils/strip-js-comments.ts
14651
- const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
14652
- const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
14653
- const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
14654
- //#endregion
14655
- //#region src/plugin/utils/is-barrel-index-module.ts
14656
- const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
14657
- const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
14658
- const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
14659
- const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
14660
- const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
14661
- const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
14662
- const createNonBarrelInfo = () => ({
14663
- isBarrel: false,
14664
- exportsByName: /* @__PURE__ */ new Map(),
14665
- starExportSources: []
14666
- });
14667
- const addImportedBinding = (importedBindings, binding) => {
14668
- importedBindings.set(binding.localName, {
14669
- ...binding,
14670
- didExport: false
14671
- });
14672
- };
14673
- const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
14674
- for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
14675
- localName: specifier.exportedName,
14676
- importedName: specifier.localName,
14677
- source,
14678
- isTypeOnly: specifier.isTypeOnly
14679
- });
14680
- };
14681
- const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
14682
- const trimmedImportClause = importClause.trim();
14683
- const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
14684
- if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
14685
- localName: namespaceMatch[1],
14686
- importedName: "*",
14687
- source,
14688
- isTypeOnly: declarationIsTypeOnly
14689
- });
14690
- const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
14691
- if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
14692
- const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
14693
- if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
14694
- localName: defaultImportName,
14695
- importedName: "default",
14696
- source,
14697
- isTypeOnly: declarationIsTypeOnly
14698
- });
14699
- };
14700
- const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
14701
- let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
14702
- collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
14703
- return "";
14704
- });
14705
- withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
14706
- const isTypeOnly = Boolean(typeKeyword);
14707
- if (namespaceExportName) {
14708
- exportsByName.set(namespaceExportName, {
14709
- exportedName: namespaceExportName,
14710
- importedName: "*",
14711
- source,
14712
- isTypeOnly
14713
- });
14714
- return "";
14715
- }
14716
- if (specifiersText) {
14717
- for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
14718
- exportedName: specifier.exportedName,
14719
- importedName: specifier.localName,
14720
- source,
14721
- isTypeOnly: specifier.isTypeOnly
14722
- });
14723
- return "";
14724
- }
14725
- starExportSources.push(source);
14726
- return "";
14727
- });
14728
- withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1, (_match, typeKeyword, specifiersText) => {
14729
- for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
14730
- const importedBinding = importedBindings.get(specifier.localName);
14731
- if (!importedBinding) return _match;
14732
- importedBinding.didExport = true;
14733
- exportsByName.set(specifier.exportedName, {
14734
- exportedName: specifier.exportedName,
14735
- importedName: importedBinding.importedName,
14736
- source: importedBinding.source,
14737
- isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
14738
- });
14739
- }
14740
- return "";
14741
- });
14742
- return withoutKnownDeclarations;
14743
- };
14744
- const hasUnexportedRuntimeImport = (importedBindings) => {
14745
- for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
14746
- return false;
14747
- };
14748
- const classifyBarrelModule = (sourceText) => {
14749
- const strippedSource = stripJsComments(sourceText).trim();
14750
- if (!strippedSource) return createNonBarrelInfo();
14751
- const importedBindings = /* @__PURE__ */ new Map();
14752
- const exportsByName = /* @__PURE__ */ new Map();
14753
- const starExportSources = [];
14754
- if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
14755
- return {
14756
- isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
14757
- exportsByName,
14758
- starExportSources
14759
- };
14760
- };
14761
- const getBarrelIndexModuleInfo = (filePath) => {
14762
- if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
14763
- const cachedResult = barrelIndexModuleInfoCache.get(filePath);
14764
- if (cachedResult !== void 0) return cachedResult;
14765
- let moduleInfo = createNonBarrelInfo();
14766
- try {
14767
- moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
14768
- } catch {
14769
- moduleInfo = createNonBarrelInfo();
14770
- }
14771
- barrelIndexModuleInfoCache.set(filePath, moduleInfo);
14772
- return moduleInfo;
14773
- };
14774
- const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
14775
- //#endregion
14776
- //#region src/plugin/utils/does-module-export-name.ts
14777
- const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
14778
- const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
14779
- const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
14780
- const doesSourceTextExportName = (sourceText, exportedName) => {
14781
- const strippedSource = stripJsComments(sourceText);
14782
- if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
14783
- for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
14784
- for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
14785
- return false;
14786
- };
14787
- const doesModuleExportName = (filePath, exportedName) => {
14788
- try {
14789
- return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
14790
- } catch {
14791
- return false;
14792
- }
14793
- };
14794
- //#endregion
14795
- //#region src/plugin/utils/resolve-relative-import-path.ts
14796
- const MODULE_FILE_EXTENSIONS = [
14797
- ".ts",
14798
- ".tsx",
14799
- ".js",
14800
- ".jsx",
14801
- ".mjs",
14802
- ".cjs",
14803
- ".mts",
14804
- ".cts"
14805
- ];
14806
- const PACKAGE_EXPORT_CONDITIONS = [
14807
- "import",
14808
- "default",
14809
- "module",
14810
- "browser",
14811
- "require"
14812
- ];
14813
- const PACKAGE_ENTRY_FIELDS = [
14814
- "module",
14815
- "main",
14816
- "browser"
14817
- ];
14818
- const getExistingFilePath = (filePath) => {
14819
- try {
14820
- return fs.statSync(filePath).isFile() ? filePath : null;
14821
- } catch {
14822
- return null;
14823
- }
14824
- };
14825
- const getExistingDirectoryPath = (directoryPath) => {
14826
- try {
14827
- return fs.statSync(directoryPath).isDirectory() ? directoryPath : null;
14828
- } catch {
14829
- return null;
14830
- }
14831
- };
14832
- const getModuleFilePathCandidates = (modulePath) => {
14833
- const extension = path.extname(modulePath);
14834
- if (!extension) return MODULE_FILE_EXTENSIONS.map((moduleExtension) => `${modulePath}${moduleExtension}`);
14835
- const modulePathWithoutExtension = modulePath.slice(0, -extension.length);
14836
- if (extension === ".js") return [
14837
- modulePath,
14838
- `${modulePathWithoutExtension}.ts`,
14839
- `${modulePathWithoutExtension}.tsx`,
14840
- `${modulePathWithoutExtension}.jsx`
14841
- ];
14842
- if (extension === ".jsx") return [modulePath, `${modulePathWithoutExtension}.tsx`];
14843
- if (extension === ".mjs") return [modulePath, `${modulePathWithoutExtension}.mts`];
14844
- if (extension === ".cjs") return [modulePath, `${modulePathWithoutExtension}.cts`];
14845
- return [modulePath];
14846
- };
14847
- const isObjectRecord = (value) => typeof value === "object" && value !== null;
14848
- const getConditionalExportEntry = (exportEntry) => {
14849
- if (typeof exportEntry === "string") return exportEntry;
14850
- if (Array.isArray(exportEntry)) {
14851
- for (const fallbackEntry of exportEntry) {
14852
- const resolvedFallbackEntry = getConditionalExportEntry(fallbackEntry);
14853
- if (resolvedFallbackEntry) return resolvedFallbackEntry;
14854
- }
14855
- return null;
14856
- }
14857
- if (!isObjectRecord(exportEntry)) return null;
14858
- for (const condition of PACKAGE_EXPORT_CONDITIONS) {
14859
- const nestedEntry = getConditionalExportEntry(exportEntry[condition]);
14860
- if (nestedEntry) return nestedEntry;
14861
- }
14862
- return null;
14863
- };
14864
- const getPackageExportEntry = (packageJson) => {
14865
- const exportsField = packageJson.exports;
14866
- if (!exportsField) return null;
14867
- const directExportEntry = getConditionalExportEntry(exportsField);
14868
- if (directExportEntry) return directExportEntry;
14869
- if (!isObjectRecord(exportsField)) return null;
14870
- return getConditionalExportEntry(exportsField["."]);
14871
- };
14872
- const resolveModulePathWithIndexFallback = (modulePath) => {
14873
- const filePath = resolveModuleFilePath(modulePath);
14874
- if (filePath) return filePath;
14875
- return resolveModuleFilePath(path.join(modulePath, "index"));
14876
- };
14877
- const resolvePackageDirectoryEntry = (directoryPath) => {
14878
- const existingDirectoryPath = getExistingDirectoryPath(directoryPath);
14879
- if (!existingDirectoryPath) return null;
14880
- const packageJsonPath = path.join(existingDirectoryPath, "package.json");
14881
- try {
14882
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
14883
- const packageEntry = getPackageExportEntry(packageJson) ?? PACKAGE_ENTRY_FIELDS.map((fieldName) => packageJson[fieldName]).find((value) => typeof value === "string");
14884
- if (!packageEntry) return null;
14885
- return resolveModulePathWithIndexFallback(path.resolve(existingDirectoryPath, packageEntry));
14886
- } catch {
14887
- return null;
14888
- }
14889
- };
14890
- const resolveModuleFilePath = (modulePath) => {
14891
- const exactFilePath = getExistingFilePath(modulePath);
14892
- if (exactFilePath) return exactFilePath;
14893
- for (const candidateFilePath of getModuleFilePathCandidates(modulePath)) {
14894
- const filePath = getExistingFilePath(candidateFilePath);
14895
- if (filePath) return filePath;
14896
- }
14897
- return null;
14898
- };
14899
- const resolveRelativeImportPath = (filename, source) => {
14900
- const importPath = path.resolve(path.dirname(filename), source);
14901
- const directFilePath = resolveModuleFilePath(importPath);
14902
- if (directFilePath) return directFilePath;
14903
- const packageEntryFilePath = resolvePackageDirectoryEntry(importPath);
14904
- if (packageEntryFilePath) return packageEntryFilePath;
14905
- return resolveModuleFilePath(path.join(importPath, "index"));
14906
- };
14907
- //#endregion
14908
- //#region src/plugin/utils/resolve-barrel-export-file-path.ts
14909
- const getUniqueFilePath = (filePaths) => {
14910
- const uniqueFilePaths = new Set(filePaths);
14911
- if (uniqueFilePaths.size !== 1) return null;
14912
- const [filePath] = uniqueFilePaths;
14913
- return filePath ?? null;
14914
- };
14915
- const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
14916
- const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
14917
- if (!resolvedTargetPath) return null;
14918
- const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
14919
- if (nestedTargetPath) return nestedTargetPath;
14920
- return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
14921
- };
14922
- const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
14923
- if (visitedFilePaths.has(barrelFilePath)) return null;
14924
- visitedFilePaths.add(barrelFilePath);
14925
- const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
14926
- if (!moduleInfo.isBarrel) return null;
14927
- const target = moduleInfo.exportsByName.get(exportedName);
14928
- if (target) {
14929
- const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
14930
- if (!resolvedTargetPath) return null;
14931
- return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
14932
- }
14933
- if (exportedName === "default") return null;
14934
- return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
14935
- };
14936
- //#endregion
14937
15437
  //#region src/plugin/rules/bundle-size/no-barrel-import.ts
14938
15438
  const getLiteralName = (node) => {
14939
15439
  if (node.type === "Identifier" && typeof node.name === "string") return node.name;
@@ -15145,7 +15645,7 @@ const noChainStateUpdates = defineRule({
15145
15645
  });
15146
15646
  //#endregion
15147
15647
  //#region src/plugin/rules/react-builtins/no-children-prop.ts
15148
- const MESSAGE$26 = "Your component can render the wrong children when you pass them through a `children` prop.";
15648
+ const MESSAGE$27 = "Your component can render the wrong children when you pass them through a `children` prop.";
15149
15649
  const noChildrenProp = defineRule({
15150
15650
  id: "no-children-prop",
15151
15651
  title: "Children passed as a prop",
@@ -15157,7 +15657,7 @@ const noChildrenProp = defineRule({
15157
15657
  if (node.name.name !== "children") return;
15158
15658
  context.report({
15159
15659
  node: node.name,
15160
- message: MESSAGE$26
15660
+ message: MESSAGE$27
15161
15661
  });
15162
15662
  },
15163
15663
  CallExpression(node) {
@@ -15170,7 +15670,7 @@ const noChildrenProp = defineRule({
15170
15670
  const propertyKey = property.key;
15171
15671
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
15172
15672
  node: propertyKey,
15173
- message: MESSAGE$26
15673
+ message: MESSAGE$27
15174
15674
  });
15175
15675
  }
15176
15676
  }
@@ -15178,7 +15678,7 @@ const noChildrenProp = defineRule({
15178
15678
  });
15179
15679
  //#endregion
15180
15680
  //#region src/plugin/rules/react-builtins/no-clone-element.ts
15181
- const MESSAGE$25 = "`React.cloneElement` breaks easily when the cloned element's props change.";
15681
+ const MESSAGE$26 = "`React.cloneElement` breaks easily when the cloned element's props change.";
15182
15682
  const noCloneElement = defineRule({
15183
15683
  id: "no-clone-element",
15184
15684
  title: "Use of cloneElement",
@@ -15191,7 +15691,7 @@ const noCloneElement = defineRule({
15191
15691
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
15192
15692
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
15193
15693
  node: callee,
15194
- message: MESSAGE$25
15694
+ message: MESSAGE$26
15195
15695
  });
15196
15696
  return;
15197
15697
  }
@@ -15204,7 +15704,7 @@ const noCloneElement = defineRule({
15204
15704
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
15205
15705
  context.report({
15206
15706
  node: callee,
15207
- message: MESSAGE$25
15707
+ message: MESSAGE$26
15208
15708
  });
15209
15709
  }
15210
15710
  } })
@@ -15253,7 +15753,7 @@ const enclosingComponentOrHookName = (node) => {
15253
15753
  };
15254
15754
  //#endregion
15255
15755
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
15256
- const MESSAGE$24 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
15756
+ const MESSAGE$25 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
15257
15757
  const CONTEXT_MODULES = [
15258
15758
  "react",
15259
15759
  "use-context-selector",
@@ -15289,7 +15789,7 @@ const noCreateContextInRender = defineRule({
15289
15789
  if (!componentOrHookName) return;
15290
15790
  context.report({
15291
15791
  node,
15292
- message: `${MESSAGE$24} (called inside "${componentOrHookName}")`
15792
+ message: `${MESSAGE$25} (called inside "${componentOrHookName}")`
15293
15793
  });
15294
15794
  } })
15295
15795
  });
@@ -15429,7 +15929,7 @@ const noCreateStoreInRender = defineRule({
15429
15929
  });
15430
15930
  //#endregion
15431
15931
  //#region src/plugin/rules/react-builtins/no-danger.ts
15432
- const MESSAGE$23 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
15932
+ const MESSAGE$24 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
15433
15933
  const noDanger = defineRule({
15434
15934
  id: "no-danger",
15435
15935
  title: "Use of dangerouslySetInnerHTML",
@@ -15442,7 +15942,7 @@ const noDanger = defineRule({
15442
15942
  if (!propAttribute) return;
15443
15943
  context.report({
15444
15944
  node: propAttribute.name,
15445
- message: MESSAGE$23
15945
+ message: MESSAGE$24
15446
15946
  });
15447
15947
  },
15448
15948
  CallExpression(node) {
@@ -15454,7 +15954,7 @@ const noDanger = defineRule({
15454
15954
  const propertyKey = property.key;
15455
15955
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
15456
15956
  node: propertyKey,
15457
- message: MESSAGE$23
15957
+ message: MESSAGE$24
15458
15958
  });
15459
15959
  }
15460
15960
  }
@@ -15462,7 +15962,7 @@ const noDanger = defineRule({
15462
15962
  });
15463
15963
  //#endregion
15464
15964
  //#region src/plugin/rules/react-builtins/no-danger-with-children.ts
15465
- const MESSAGE$22 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
15965
+ const MESSAGE$23 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
15466
15966
  const isLineBreak = (child) => {
15467
15967
  if (!isNodeOfType(child, "JSXText")) return false;
15468
15968
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -15532,7 +16032,7 @@ const noDangerWithChildren = defineRule({
15532
16032
  if (!hasChildrenProp && !hasNestedChildren) return;
15533
16033
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
15534
16034
  node: opening,
15535
- message: MESSAGE$22
16035
+ message: MESSAGE$23
15536
16036
  });
15537
16037
  },
15538
16038
  CallExpression(node) {
@@ -15544,7 +16044,7 @@ const noDangerWithChildren = defineRule({
15544
16044
  if (!propsShape.hasDangerously) return;
15545
16045
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
15546
16046
  node,
15547
- message: MESSAGE$22
16047
+ message: MESSAGE$23
15548
16048
  });
15549
16049
  }
15550
16050
  })
@@ -16122,7 +16622,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
16122
16622
  //#endregion
16123
16623
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
16124
16624
  const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
16125
- const MESSAGE$21 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
16625
+ const MESSAGE$22 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
16126
16626
  const resolveSettings$20 = (settings) => {
16127
16627
  const reactDoctor = settings?.["react-doctor"];
16128
16628
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -16141,7 +16641,7 @@ const noDidMountSetState = defineRule({
16141
16641
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
16142
16642
  context.report({
16143
16643
  node: node.callee,
16144
- message: MESSAGE$21
16644
+ message: MESSAGE$22
16145
16645
  });
16146
16646
  } };
16147
16647
  }
@@ -16149,7 +16649,7 @@ const noDidMountSetState = defineRule({
16149
16649
  //#endregion
16150
16650
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
16151
16651
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
16152
- const MESSAGE$20 = "This can loop forever & freeze the component.";
16652
+ const MESSAGE$21 = "This can loop forever & freeze the component.";
16153
16653
  const resolveSettings$19 = (settings) => {
16154
16654
  const reactDoctor = settings?.["react-doctor"];
16155
16655
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -16168,7 +16668,7 @@ const noDidUpdateSetState = defineRule({
16168
16668
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
16169
16669
  context.report({
16170
16670
  node: node.callee,
16171
- message: MESSAGE$20
16671
+ message: MESSAGE$21
16172
16672
  });
16173
16673
  } };
16174
16674
  }
@@ -16191,7 +16691,7 @@ const isStateMemberExpression = (node) => {
16191
16691
  };
16192
16692
  //#endregion
16193
16693
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
16194
- const MESSAGE$19 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
16694
+ const MESSAGE$20 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
16195
16695
  const shouldIgnoreMutation = (node) => {
16196
16696
  let isConstructor = false;
16197
16697
  let isInsideCallExpression = false;
@@ -16213,7 +16713,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
16213
16713
  if (shouldIgnoreMutation(reportNode)) return;
16214
16714
  context.report({
16215
16715
  node: reportNode,
16216
- message: MESSAGE$19
16716
+ message: MESSAGE$20
16217
16717
  });
16218
16718
  };
16219
16719
  const noDirectMutationState = defineRule({
@@ -17801,7 +18301,7 @@ const ALLOWED_NAMESPACES = new Set([
17801
18301
  "ReactDOM",
17802
18302
  "ReactDom"
17803
18303
  ]);
17804
- const MESSAGE$18 = "`findDOMNode` crashes your app in React 19 because it was removed.";
18304
+ const MESSAGE$19 = "`findDOMNode` crashes your app in React 19 because it was removed.";
17805
18305
  const noFindDomNode = defineRule({
17806
18306
  id: "no-find-dom-node",
17807
18307
  title: "Use of findDOMNode",
@@ -17812,7 +18312,7 @@ const noFindDomNode = defineRule({
17812
18312
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
17813
18313
  context.report({
17814
18314
  node: callee,
17815
- message: MESSAGE$18
18315
+ message: MESSAGE$19
17816
18316
  });
17817
18317
  return;
17818
18318
  }
@@ -17823,7 +18323,7 @@ const noFindDomNode = defineRule({
17823
18323
  if (callee.property.name !== "findDOMNode") return;
17824
18324
  context.report({
17825
18325
  node: callee.property,
17826
- message: MESSAGE$18
18326
+ message: MESSAGE$19
17827
18327
  });
17828
18328
  }
17829
18329
  } })
@@ -18352,6 +18852,51 @@ const noIsMounted = defineRule({
18352
18852
  } })
18353
18853
  });
18354
18854
  //#endregion
18855
+ //#region src/plugin/rules/correctness/no-jsx-element-type.ts
18856
+ const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
18857
+ const isJsxElementTypeReference = (node) => {
18858
+ if (!isNodeOfType(node, "TSTypeReference")) return false;
18859
+ const typeName = node.typeName;
18860
+ if (!isNodeOfType(typeName, "TSQualifiedName")) return false;
18861
+ return isNodeOfType(typeName.left, "Identifier") && typeName.left.name === "JSX" && isNodeOfType(typeName.right, "Identifier") && typeName.right.name === "Element";
18862
+ };
18863
+ const extractReturnTypeAnnotation = (returnType) => {
18864
+ if (!returnType) return null;
18865
+ if (!isNodeOfType(returnType, "TSTypeAnnotation")) return null;
18866
+ return returnType.typeAnnotation ?? null;
18867
+ };
18868
+ const checkReturnType = (context, returnType) => {
18869
+ const typeAnnotation = extractReturnTypeAnnotation(returnType);
18870
+ if (!typeAnnotation) return;
18871
+ if (isJsxElementTypeReference(typeAnnotation)) context.report({
18872
+ node: typeAnnotation,
18873
+ message: MESSAGE$18
18874
+ });
18875
+ };
18876
+ const noJsxElementType = defineRule({
18877
+ id: "no-jsx-element-type",
18878
+ title: "No JSX.Element",
18879
+ severity: "error",
18880
+ recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
18881
+ create: (context) => ({
18882
+ FunctionDeclaration(node) {
18883
+ checkReturnType(context, node.returnType);
18884
+ },
18885
+ ArrowFunctionExpression(node) {
18886
+ checkReturnType(context, node.returnType);
18887
+ },
18888
+ FunctionExpression(node) {
18889
+ checkReturnType(context, node.returnType);
18890
+ },
18891
+ TSDeclareFunction(node) {
18892
+ checkReturnType(context, node.returnType);
18893
+ },
18894
+ TSMethodSignature(node) {
18895
+ checkReturnType(context, node.returnType);
18896
+ }
18897
+ })
18898
+ });
18899
+ //#endregion
18355
18900
  //#region src/plugin/rules/design/no-justified-text.ts
18356
18901
  const noJustifiedText = defineRule({
18357
18902
  id: "no-justified-text",
@@ -18623,11 +19168,14 @@ const noLongTransitionDuration = defineRule({
18623
19168
  } })
18624
19169
  });
18625
19170
  //#endregion
19171
+ //#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
19172
+ const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
19173
+ const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
19174
+ //#endregion
18626
19175
  //#region src/plugin/utils/is-component-declaration.ts
18627
19176
  const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
18628
19177
  //#endregion
18629
19178
  //#region src/plugin/rules/architecture/no-many-boolean-props.ts
18630
- const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
18631
19179
  const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
18632
19180
  const found = /* @__PURE__ */ new Set();
18633
19181
  if (!componentBody) return found;
@@ -18637,7 +19185,7 @@ const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
18637
19185
  if (!isNodeOfType(child.object, "Identifier")) return;
18638
19186
  if (child.object.name !== propsParamName) return;
18639
19187
  if (!isNodeOfType(child.property, "Identifier")) return;
18640
- if (!BOOLEAN_PROP_PREFIX_PATTERN.test(child.property.name)) return;
19188
+ if (!isBooleanPrefixedPropName(child.property.name)) return;
18641
19189
  found.add(child.property.name);
18642
19190
  });
18643
19191
  return found;
@@ -18663,7 +19211,7 @@ const noManyBooleanProps = defineRule({
18663
19211
  if (!isNodeOfType(property, "Property")) continue;
18664
19212
  const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
18665
19213
  if (!keyName) continue;
18666
- if (BOOLEAN_PROP_PREFIX_PATTERN.test(keyName)) booleanLikePropNames.push(keyName);
19214
+ if (isBooleanPrefixedPropName(keyName)) booleanLikePropNames.push(keyName);
18667
19215
  }
18668
19216
  reportIfMany(booleanLikePropNames, componentName, reportNode);
18669
19217
  return;
@@ -19233,224 +19781,7 @@ const isLodashMutatorCall = (callExpression) => {
19233
19781
  return false;
19234
19782
  };
19235
19783
  //#endregion
19236
- //#region src/plugin/utils/find-exported-function-body.ts
19237
- const isFunctionLike = (node) => {
19238
- if (!node) return false;
19239
- return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
19240
- };
19241
- const findExportedFunctionBody = (programRoot, exportedName) => {
19242
- if (!isNodeOfType(programRoot, "Program")) return null;
19243
- const localBindings = /* @__PURE__ */ new Map();
19244
- const namedExports = /* @__PURE__ */ new Map();
19245
- let defaultExport = null;
19246
- let defaultExportIdentifierName = null;
19247
- const recordVariableDeclaration = (declaration) => {
19248
- for (const declarator of declaration.declarations ?? []) {
19249
- if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
19250
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
19251
- const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
19252
- if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
19253
- }
19254
- };
19255
- for (const statement of programRoot.body ?? []) {
19256
- if (isNodeOfType(statement, "VariableDeclaration")) {
19257
- recordVariableDeclaration(statement);
19258
- continue;
19259
- }
19260
- if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) {
19261
- localBindings.set(statement.id.name, statement);
19262
- continue;
19263
- }
19264
- if (isNodeOfType(statement, "ExportNamedDeclaration")) {
19265
- const declaration = statement.declaration;
19266
- if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
19267
- recordVariableDeclaration(declaration);
19268
- for (const declarator of declaration.declarations ?? []) {
19269
- if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
19270
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
19271
- namedExports.set(declarator.id.name, declarator.id.name);
19272
- }
19273
- } else if (declaration && isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
19274
- localBindings.set(declaration.id.name, declaration);
19275
- namedExports.set(declaration.id.name, declaration.id.name);
19276
- }
19277
- for (const specifier of statement.specifiers ?? []) {
19278
- if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
19279
- const local = specifier.local;
19280
- const exported = specifier.exported;
19281
- if (!isNodeOfType(local, "Identifier")) continue;
19282
- const exportedNameSpec = isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null;
19283
- if (!exportedNameSpec) continue;
19284
- namedExports.set(exportedNameSpec, local.name);
19285
- }
19286
- continue;
19287
- }
19288
- if (isNodeOfType(statement, "ExportDefaultDeclaration")) {
19289
- const declaration = statement.declaration;
19290
- if (!declaration) continue;
19291
- if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
19292
- localBindings.set(declaration.id.name, declaration);
19293
- defaultExport = declaration;
19294
- continue;
19295
- }
19296
- if (isFunctionLike(declaration)) {
19297
- defaultExport = declaration;
19298
- continue;
19299
- }
19300
- if (isNodeOfType(declaration, "Identifier")) {
19301
- defaultExportIdentifierName = declaration.name;
19302
- continue;
19303
- }
19304
- }
19305
- }
19306
- if (exportedName === "default") {
19307
- if (defaultExport) return defaultExport;
19308
- if (defaultExportIdentifierName) {
19309
- const binding = localBindings.get(defaultExportIdentifierName);
19310
- if (binding) return binding;
19311
- }
19312
- }
19313
- const localName = namedExports.get(exportedName);
19314
- if (!localName) return null;
19315
- return localBindings.get(localName) ?? null;
19316
- };
19317
- const resolveImportedExportName = (importSpecifier) => {
19318
- if (isNodeOfType(importSpecifier, "ImportSpecifier")) {
19319
- const imported = importSpecifier.imported;
19320
- if (isNodeOfType(imported, "Identifier")) return imported.name;
19321
- if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
19322
- return null;
19323
- }
19324
- if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
19325
- return null;
19326
- };
19327
- const findReExportSourcesForName = (programRoot, exportedName) => {
19328
- if (!isNodeOfType(programRoot, "Program")) return [];
19329
- const exportAllSources = [];
19330
- for (const statement of programRoot.body ?? []) {
19331
- if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
19332
- const sourceValue = statement.source.value;
19333
- if (typeof sourceValue !== "string") continue;
19334
- for (const specifier of statement.specifiers ?? []) {
19335
- if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
19336
- const exported = specifier.exported;
19337
- if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
19338
- }
19339
- }
19340
- if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
19341
- const sourceValue = statement.source.value;
19342
- if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
19343
- }
19344
- }
19345
- return exportAllSources;
19346
- };
19347
- //#endregion
19348
- //#region src/plugin/utils/attach-parent-references.ts
19349
- const attachParentReferences = (root) => {
19350
- const visit = (node, parent) => {
19351
- const writableNode = node;
19352
- writableNode.parent = parent;
19353
- const nodeRecord = node;
19354
- for (const key of Object.keys(nodeRecord)) {
19355
- if (key === "parent") continue;
19356
- const child = nodeRecord[key];
19357
- if (Array.isArray(child)) {
19358
- for (const item of child) if (isAstNode(item)) visit(item, node);
19359
- } else if (isAstNode(child)) visit(child, node);
19360
- }
19361
- };
19362
- visit(root, null);
19363
- };
19364
- //#endregion
19365
- //#region src/plugin/utils/parse-source-file.ts
19366
- const FILENAME_TO_LANG = {
19367
- ".ts": "ts",
19368
- ".tsx": "tsx",
19369
- ".js": "js",
19370
- ".jsx": "jsx",
19371
- ".mjs": "js",
19372
- ".cjs": "js",
19373
- ".mts": "ts",
19374
- ".cts": "ts"
19375
- };
19376
- const resolveLang = (filename) => {
19377
- return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
19378
- };
19379
- const parseCache = /* @__PURE__ */ new Map();
19380
- const parseSourceFile = (absoluteFilePath) => {
19381
- let fileStat;
19382
- try {
19383
- fileStat = fs.statSync(absoluteFilePath);
19384
- } catch {
19385
- return null;
19386
- }
19387
- if (!fileStat.isFile()) return null;
19388
- if (fileStat.size > 2e6) return null;
19389
- const cached = parseCache.get(absoluteFilePath);
19390
- if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
19391
- if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
19392
- parseCache.set(absoluteFilePath, {
19393
- mtimeMs: fileStat.mtimeMs,
19394
- size: fileStat.size,
19395
- program: null
19396
- });
19397
- return null;
19398
- }
19399
- let sourceText;
19400
- try {
19401
- sourceText = fs.readFileSync(absoluteFilePath, "utf8");
19402
- } catch {
19403
- parseCache.set(absoluteFilePath, {
19404
- mtimeMs: fileStat.mtimeMs,
19405
- size: fileStat.size,
19406
- program: null
19407
- });
19408
- return null;
19409
- }
19410
- let parsedProgram = null;
19411
- try {
19412
- const result = parseSync(absoluteFilePath, sourceText, {
19413
- astType: "ts",
19414
- lang: resolveLang(absoluteFilePath)
19415
- });
19416
- if (!result.errors.some((parseError) => parseError.severity === "Error")) {
19417
- parsedProgram = result.program;
19418
- attachParentReferences(parsedProgram);
19419
- }
19420
- } catch {
19421
- parsedProgram = null;
19422
- }
19423
- parseCache.set(absoluteFilePath, {
19424
- mtimeMs: fileStat.mtimeMs,
19425
- size: fileStat.size,
19426
- program: parsedProgram
19427
- });
19428
- return parsedProgram;
19429
- };
19430
- //#endregion
19431
19784
  //#region src/plugin/rules/state-and-effects/utils/resolve-reducer-function.ts
19432
- const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
19433
- if (visitedFilePaths.size >= 4) return null;
19434
- if (visitedFilePaths.has(filePath)) return null;
19435
- visitedFilePaths.add(filePath);
19436
- const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
19437
- const programRoot = parseSourceFile(actualFilePath);
19438
- if (!programRoot) return null;
19439
- const exported = findExportedFunctionBody(programRoot, exportedName);
19440
- if (exported) return exported;
19441
- for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
19442
- const nextFilePath = resolveRelativeImportPath(actualFilePath, reExportSource);
19443
- if (!nextFilePath) continue;
19444
- const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
19445
- if (resolved) return resolved;
19446
- }
19447
- return null;
19448
- };
19449
- const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
19450
- const resolvedFilePath = resolveRelativeImportPath(fromFilename, source);
19451
- if (!resolvedFilePath) return null;
19452
- return resolveFunctionExportInFile(resolvedFilePath, exportedName, /* @__PURE__ */ new Set());
19453
- };
19454
19785
  const resolveReducerFunction = (node, currentFilename) => {
19455
19786
  if (!node) return null;
19456
19787
  const unwrappedNode = stripParenExpression(node);
@@ -19472,7 +19803,6 @@ const resolveReducerFunction = (node, currentFilename) => {
19472
19803
  if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
19473
19804
  const sourceValue = importDeclaration.source?.value;
19474
19805
  if (typeof sourceValue !== "string") return null;
19475
- if (!sourceValue.startsWith(".") && !sourceValue.startsWith("/")) return null;
19476
19806
  const exportedName = resolveImportedExportName(initializer);
19477
19807
  if (!exportedName) return null;
19478
19808
  const crossFileFunction = resolveCrossFileFunctionExport(currentFilename, sourceValue, exportedName);
@@ -23472,6 +23802,7 @@ const noUnknownProperty = defineRule({
23472
23802
  create: (context) => {
23473
23803
  const { ignore = [], requireDataLowercase = false } = resolveSettings$10(context.settings);
23474
23804
  const ignoreSet = new Set(ignore);
23805
+ if (isNextjsMetadataImageRouteFilename(context.filename)) ignoreSet.add("tw");
23475
23806
  let fileIsNonReactJsx = false;
23476
23807
  return {
23477
23808
  Program(node) {
@@ -24166,6 +24497,8 @@ const ENTRY_POINT_BASENAMES = new Set([
24166
24497
  "global-error.jsx",
24167
24498
  "route.tsx",
24168
24499
  "route.jsx",
24500
+ "_layout.tsx",
24501
+ "_layout.jsx",
24169
24502
  "_app.tsx",
24170
24503
  "_app.jsx",
24171
24504
  "_document.tsx",
@@ -24874,6 +25207,128 @@ const preferEs6Class = defineRule({
24874
25207
  }
24875
25208
  });
24876
25209
  //#endregion
25210
+ //#region src/plugin/utils/is-jsx-element-or-fragment.ts
25211
+ /**
25212
+ * Type-guard for the two single-node JSX output forms: `JSXElement`
25213
+ * (`<Foo />`) and `JSXFragment` (`<>…</>`). Canonical home for the
25214
+ * `isNodeOfType(x, "JSXElement") || isNodeOfType(x, "JSXFragment")` check
25215
+ * that many rules otherwise inline. Does NOT unwrap parens / TS wrappers —
25216
+ * callers that need the semantic expression should `stripParenExpression`
25217
+ * first.
25218
+ */
25219
+ const isJsxElementOrFragment = (node) => Boolean(node && (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")));
25220
+ //#endregion
25221
+ //#region src/plugin/rules/architecture/prefer-explicit-variants.ts
25222
+ const resolveBooleanPropTestName = (testNode, booleanPropBindings) => {
25223
+ let identifierNode = stripParenExpression(testNode);
25224
+ if (isNodeOfType(identifierNode, "UnaryExpression") && identifierNode.operator === "!") identifierNode = stripParenExpression(identifierNode.argument);
25225
+ if (!isNodeOfType(identifierNode, "Identifier")) return null;
25226
+ return booleanPropBindings.has(identifierNode.name) ? identifierNode.name : null;
25227
+ };
25228
+ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
25229
+ "isLoading",
25230
+ "isPending",
25231
+ "isFetching",
25232
+ "isRefetching",
25233
+ "isSubmitting",
25234
+ "isError",
25235
+ "isSuccess",
25236
+ "isEmpty",
25237
+ "isReady",
25238
+ "isDirty",
25239
+ "isValid",
25240
+ "isInvalid",
25241
+ "isOpen",
25242
+ "isClosed",
25243
+ "isVisible",
25244
+ "isHidden",
25245
+ "isActive",
25246
+ "isInactive",
25247
+ "isExpanded",
25248
+ "isCollapsed",
25249
+ "isSelected",
25250
+ "isChecked",
25251
+ "isDisabled",
25252
+ "isEnabled",
25253
+ "isFocused",
25254
+ "isHovered",
25255
+ "isDragging",
25256
+ "isFullscreen",
25257
+ "isMobile",
25258
+ "isDesktop",
25259
+ "isTablet",
25260
+ "isOnline",
25261
+ "isOffline",
25262
+ "isLoggedIn",
25263
+ "isAuthenticated",
25264
+ "isAuthorized",
25265
+ "isDark",
25266
+ "isLight"
25267
+ ]);
25268
+ const collectBooleanPropBindings = (param) => {
25269
+ const bindings = /* @__PURE__ */ new Set();
25270
+ if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
25271
+ for (const property of param.properties ?? []) {
25272
+ if (!isNodeOfType(property, "Property")) continue;
25273
+ if (property.computed) continue;
25274
+ if (!isNodeOfType(property.key, "Identifier")) continue;
25275
+ if (!isBooleanPrefixedPropName(property.key.name)) continue;
25276
+ if (CROSS_CUTTING_STATE_BOOLEAN_NAMES.has(property.key.name)) continue;
25277
+ const propertyValue = property.value;
25278
+ if (isNodeOfType(propertyValue, "Identifier")) bindings.add(propertyValue.name);
25279
+ else if (isNodeOfType(propertyValue, "AssignmentPattern") && isNodeOfType(propertyValue.left, "Identifier")) bindings.add(propertyValue.left.name);
25280
+ }
25281
+ return bindings;
25282
+ };
25283
+ const collectVariantBranchProps = (body, booleanPropBindings) => {
25284
+ const variantBranchProps = /* @__PURE__ */ new Set();
25285
+ if (!body) return variantBranchProps;
25286
+ walkAst(body, (current) => {
25287
+ if (isNodeOfType(current, "FunctionDeclaration") || isInlineFunctionExpression(current)) return false;
25288
+ if (!isNodeOfType(current, "ConditionalExpression")) return;
25289
+ const propName = resolveBooleanPropTestName(current.test, booleanPropBindings);
25290
+ if (!propName) return;
25291
+ const consequent = stripParenExpression(current.consequent);
25292
+ const alternate = stripParenExpression(current.alternate);
25293
+ if (!isJsxElementOrFragment(consequent) || !isJsxElementOrFragment(alternate)) return;
25294
+ variantBranchProps.add(propName);
25295
+ });
25296
+ return variantBranchProps;
25297
+ };
25298
+ const preferExplicitVariants = defineRule({
25299
+ id: "prefer-explicit-variants",
25300
+ title: "Prefer explicit variant components",
25301
+ severity: "warn",
25302
+ tags: ["test-noise", "react-jsx-only"],
25303
+ recommendation: "Replace boolean props that switch whole subtrees with explicit variant components, like `<ThreadComposer />` and `<EditMessageComposer />`, so each variant renders one clear path.",
25304
+ create: (context) => {
25305
+ const checkComponent = (param, body, componentName, reportNode) => {
25306
+ const booleanPropBindings = collectBooleanPropBindings(param);
25307
+ if (booleanPropBindings.size < 2) return;
25308
+ const variantBranchProps = collectVariantBranchProps(body, booleanPropBindings);
25309
+ if (variantBranchProps.size < 2) return;
25310
+ const propList = [...variantBranchProps].slice(0, 3).join(", ");
25311
+ const overflow = variantBranchProps.size > 3 ? "…" : "";
25312
+ context.report({
25313
+ node: reportNode,
25314
+ message: `Component "${componentName}" picks which component to render from ${variantBranchProps.size} boolean props (${propList}${overflow}), which multiplies untestable variants. Split it into explicit variant components so each renders one clear path.`
25315
+ });
25316
+ };
25317
+ return {
25318
+ FunctionDeclaration(node) {
25319
+ if (!isComponentDeclaration(node) || !node.id) return;
25320
+ checkComponent(node.params?.[0], node.body, node.id.name, node.id);
25321
+ },
25322
+ VariableDeclarator(node) {
25323
+ if (!isComponentAssignment(node)) return;
25324
+ if (!isNodeOfType(node.id, "Identifier")) return;
25325
+ if (!isInlineFunctionExpression(node.init)) return;
25326
+ checkComponent(node.init.params?.[0], node.init.body, node.id.name, node.id);
25327
+ }
25328
+ };
25329
+ }
25330
+ });
25331
+ //#endregion
24877
25332
  //#region src/plugin/rules/react-builtins/prefer-function-component.ts
24878
25333
  const MESSAGE$7 = "This class component is harder to maintain than a function component.";
24879
25334
  const resolveSettings$4 = (settings) => {
@@ -25588,6 +26043,26 @@ const preferUseReducer = defineRule({
25588
26043
  }
25589
26044
  });
25590
26045
  //#endregion
26046
+ //#region src/plugin/rules/tanstack-query/query-destructure-result.ts
26047
+ const queryDestructureResult = defineRule({
26048
+ id: "query-destructure-result",
26049
+ title: "Destructure TanStack Query result",
26050
+ tags: ["test-noise"],
26051
+ requires: ["tanstack-query"],
26052
+ severity: "error",
26053
+ recommendation: "Destructure only the fields you need, like `const { data, isLoading } = useQuery(...)`. Assigning the whole object bypasses TanStack Query's tracked-property optimization and subscribes to every field.",
26054
+ create: (context) => ({ VariableDeclarator(node) {
26055
+ if (!isNodeOfType(node.id, "Identifier")) return;
26056
+ if (!node.init || !isNodeOfType(node.init, "CallExpression")) return;
26057
+ const calleeName = isNodeOfType(node.init.callee, "Identifier") ? node.init.callee.name : null;
26058
+ if (!calleeName || !TANSTACK_QUERY_HOOKS.has(calleeName)) return;
26059
+ context.report({
26060
+ node: node.id,
26061
+ message: `Destructure ${calleeName}() results instead of assigning the whole query object, so TanStack Query only subscribes to the fields you use.`
26062
+ });
26063
+ } })
26064
+ });
26065
+ //#endregion
25591
26066
  //#region src/plugin/rules/tanstack-query/query-mutation-missing-invalidation.ts
25592
26067
  const queryMutationMissingInvalidation = defineRule({
25593
26068
  id: "query-mutation-missing-invalidation",
@@ -33097,6 +33572,7 @@ const serverFetchWithoutRevalidate = defineRule({
33097
33572
  id: "server-fetch-without-revalidate",
33098
33573
  title: "Fetch without revalidate",
33099
33574
  severity: "warn",
33575
+ disabledBy: ["nextjs:15"],
33100
33576
  recommendation: "Pass `{ next: { revalidate: <seconds> } }` (or `cache: \"no-store\"`) so old data doesn't stick around.",
33101
33577
  create: (context) => {
33102
33578
  let isServerSideFile = false;
@@ -36436,6 +36912,17 @@ const reactDoctorRules = [
36436
36912
  category: "Bugs"
36437
36913
  }
36438
36914
  },
36915
+ {
36916
+ key: "react-doctor/no-jsx-element-type",
36917
+ id: "no-jsx-element-type",
36918
+ source: "react-doctor",
36919
+ originallyExternal: false,
36920
+ rule: {
36921
+ ...noJsxElementType,
36922
+ framework: "global",
36923
+ category: "Bugs"
36924
+ }
36925
+ },
36439
36926
  {
36440
36927
  key: "react-doctor/no-justified-text",
36441
36928
  id: "no-justified-text",
@@ -37162,6 +37649,17 @@ const reactDoctorRules = [
37162
37649
  category: "Maintainability"
37163
37650
  }
37164
37651
  },
37652
+ {
37653
+ key: "react-doctor/prefer-explicit-variants",
37654
+ id: "prefer-explicit-variants",
37655
+ source: "react-doctor",
37656
+ originallyExternal: false,
37657
+ rule: {
37658
+ ...preferExplicitVariants,
37659
+ framework: "global",
37660
+ category: "Maintainability"
37661
+ }
37662
+ },
37165
37663
  {
37166
37664
  key: "react-doctor/prefer-function-component",
37167
37665
  id: "prefer-function-component",
@@ -37261,6 +37759,17 @@ const reactDoctorRules = [
37261
37759
  category: "Bugs"
37262
37760
  }
37263
37761
  },
37762
+ {
37763
+ key: "react-doctor/query-destructure-result",
37764
+ id: "query-destructure-result",
37765
+ source: "react-doctor",
37766
+ originallyExternal: false,
37767
+ rule: {
37768
+ ...queryDestructureResult,
37769
+ framework: "tanstack-query",
37770
+ category: "Bugs"
37771
+ }
37772
+ },
37264
37773
  {
37265
37774
  key: "react-doctor/query-mutation-missing-invalidation",
37266
37775
  id: "query-mutation-missing-invalidation",