oxlint-plugin-react-doctor 0.8.1-dev.b4556e3 → 0.8.1-dev.b852961

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.
package/dist/index.d.ts CHANGED
@@ -2421,6 +2421,29 @@ declare const REACT_DOCTOR_RULES: readonly [{
2421
2421
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
2422
2422
  readonly create: (context: RuleContext) => RuleVisitors;
2423
2423
  };
2424
+ }, {
2425
+ readonly key: "react-doctor/jsx-no-target-blank";
2426
+ readonly id: "jsx-no-target-blank";
2427
+ readonly source: "react-doctor";
2428
+ readonly originallyExternal: true;
2429
+ readonly rule: {
2430
+ readonly framework: "global";
2431
+ readonly category: "Security";
2432
+ readonly requires: readonly Capability[];
2433
+ readonly id: string;
2434
+ readonly title?: string;
2435
+ readonly severity: RuleSeverity;
2436
+ readonly disabledWhen?: ReadonlyArray<Capability>;
2437
+ readonly tags?: ReadonlyArray<string>;
2438
+ readonly matchByOccurrence?: boolean;
2439
+ readonly defaultEnabled?: boolean;
2440
+ readonly lifecycle?: "retired";
2441
+ readonly scan?: FileScan;
2442
+ readonly committedFilesOnly?: boolean;
2443
+ readonly recommendation?: string;
2444
+ readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
2445
+ readonly create: (context: RuleContext) => RuleVisitors;
2446
+ };
2424
2447
  }, {
2425
2448
  readonly key: "react-doctor/jsx-no-undef";
2426
2449
  readonly id: "jsx-no-undef";
@@ -11710,6 +11733,29 @@ declare const RULES: readonly [{
11710
11733
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
11711
11734
  readonly create: (context: RuleContext) => RuleVisitors;
11712
11735
  };
11736
+ }, {
11737
+ readonly key: "react-doctor/jsx-no-target-blank";
11738
+ readonly id: "jsx-no-target-blank";
11739
+ readonly source: "react-doctor";
11740
+ readonly originallyExternal: true;
11741
+ readonly rule: {
11742
+ readonly framework: "global";
11743
+ readonly category: "Security";
11744
+ readonly requires: readonly Capability[];
11745
+ readonly id: string;
11746
+ readonly title?: string;
11747
+ readonly severity: RuleSeverity;
11748
+ readonly disabledWhen?: ReadonlyArray<Capability>;
11749
+ readonly tags?: ReadonlyArray<string>;
11750
+ readonly matchByOccurrence?: boolean;
11751
+ readonly defaultEnabled?: boolean;
11752
+ readonly lifecycle?: "retired";
11753
+ readonly scan?: FileScan;
11754
+ readonly committedFilesOnly?: boolean;
11755
+ readonly recommendation?: string;
11756
+ readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
11757
+ readonly create: (context: RuleContext) => RuleVisitors;
11758
+ };
11713
11759
  }, {
11714
11760
  readonly key: "react-doctor/jsx-no-undef";
11715
11761
  readonly id: "jsx-no-undef";
package/dist/index.js CHANGED
@@ -2622,7 +2622,7 @@ const PREFER_ALT = "Screen readers skip a decorative image more reliably with `a
2622
2622
  const MESSAGE_OBJECT = "Screen reader users cannot use this `<object>` because assistive tech cannot describe it, so add `alt`, `aria-label`, `aria-labelledby`, `title`, or inner fallback text.";
2623
2623
  const MESSAGE_AREA = "Blind users can't use this `<area>` of the image map because screen readers can't describe it, so add `alt`, `aria-label`, or `aria-labelledby`.";
2624
2624
  const MESSAGE_INPUT_IMAGE = "Blind users can't use this image button because screen readers can't describe it, so add `alt`, `aria-label`, or `aria-labelledby`.";
2625
- const resolveSettings$52 = (settings) => {
2625
+ const resolveSettings$53 = (settings) => {
2626
2626
  const reactDoctor = settings?.["react-doctor"];
2627
2627
  return typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.altText ?? {} : {};
2628
2628
  };
@@ -2745,7 +2745,7 @@ const altText = defineRule({
2745
2745
  category: "Accessibility",
2746
2746
  create: (context) => {
2747
2747
  if (isGeneratedImageRenderContext(context)) return {};
2748
- const settings = resolveSettings$52(context.settings);
2748
+ const settings = resolveSettings$53(context.settings);
2749
2749
  const checkImg = !settings.elements || settings.elements.includes("img");
2750
2750
  const checkObject = !settings.elements || settings.elements.includes("object");
2751
2751
  const checkArea = !settings.elements || settings.elements.includes("area");
@@ -2807,7 +2807,7 @@ const DEFAULT_AMBIGUOUS = [
2807
2807
  "a link",
2808
2808
  "learn more"
2809
2809
  ];
2810
- const resolveSettings$51 = (settings) => {
2810
+ const resolveSettings$52 = (settings) => {
2811
2811
  const reactDoctor = settings?.["react-doctor"];
2812
2812
  return { words: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorAmbiguousText ?? {} : {}).words ?? DEFAULT_AMBIGUOUS };
2813
2813
  };
@@ -2857,7 +2857,7 @@ const anchorAmbiguousText = defineRule({
2857
2857
  category: "Accessibility",
2858
2858
  create: (context) => {
2859
2859
  if (isTestlikeFilename(context.filename)) return {};
2860
- const settings = resolveSettings$51(context.settings);
2860
+ const settings = resolveSettings$52(context.settings);
2861
2861
  const ambiguousSet = new Set(settings.words.map((word) => word.toLowerCase()));
2862
2862
  return { JSXElement(node) {
2863
2863
  if (getElementType(node.openingElement, context.settings) !== "a") return;
@@ -3225,7 +3225,7 @@ const anchorHasContent = defineRule({
3225
3225
  const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
3226
3226
  const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
3227
3227
  const MESSAGE_CANT_BE_ANCHOR = "Keyboard users can't trigger this link because it's a click handler with no real `href`, so use `<button>` instead.";
3228
- const resolveSettings$50 = (settings) => {
3228
+ const resolveSettings$51 = (settings) => {
3229
3229
  const reactDoctor = settings?.["react-doctor"];
3230
3230
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorIsValid ?? {} : {};
3231
3231
  const jsxA11y = settings?.["jsx-a11y"];
@@ -3268,7 +3268,7 @@ const anchorIsValid = defineRule({
3268
3268
  recommendation: "Give links a real destination. Use `<button>` for in-page actions.",
3269
3269
  category: "Accessibility",
3270
3270
  create: (context) => {
3271
- const settings = resolveSettings$50(context.settings);
3271
+ const settings = resolveSettings$51(context.settings);
3272
3272
  const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
3273
3273
  const isTestlikeFile = isTestlikeFilename(context.filename);
3274
3274
  return { JSXOpeningElement(node) {
@@ -4181,7 +4181,7 @@ const ariaProptypes = defineRule({
4181
4181
  //#endregion
4182
4182
  //#region src/plugin/rules/a11y/aria-role.ts
4183
4183
  const buildBaseMessage = (suffix) => `This \`role\` is not a valid ARIA role, so assistive tech cannot expose it correctly. Use a real, non-abstract role.${suffix}`;
4184
- const resolveSettings$49 = (settings) => {
4184
+ const resolveSettings$50 = (settings) => {
4185
4185
  const reactDoctor = settings?.["react-doctor"];
4186
4186
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.ariaRole ?? {} : {};
4187
4187
  return {
@@ -4197,7 +4197,7 @@ const ariaRole = defineRule({
4197
4197
  recommendation: "Use a real, non-abstract ARIA role so assistive tech can expose the element correctly.",
4198
4198
  category: "Accessibility",
4199
4199
  create: (context) => {
4200
- const settings = resolveSettings$49(context.settings);
4200
+ const settings = resolveSettings$50(context.settings);
4201
4201
  return { JSXOpeningElement(node) {
4202
4202
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
4203
4203
  if (!roleAttribute) return;
@@ -7115,7 +7115,7 @@ const FORM_CONTROL_TAGS = new Set([
7115
7115
  "select",
7116
7116
  "form"
7117
7117
  ]);
7118
- const resolveSettings$48 = (settings) => {
7118
+ const resolveSettings$49 = (settings) => {
7119
7119
  const reactDoctor = settings?.["react-doctor"];
7120
7120
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {};
7121
7121
  return { inputComponents: new Set(ruleSettings.inputComponents ?? []) };
@@ -7128,7 +7128,7 @@ const autocompleteValid = defineRule({
7128
7128
  recommendation: "Use a valid autofill token in `autoComplete` so browsers can fill the right field reliably.",
7129
7129
  category: "Accessibility",
7130
7130
  create: skipNonProductionFiles((context) => {
7131
- const settings = resolveSettings$48(context.settings);
7131
+ const settings = resolveSettings$49(context.settings);
7132
7132
  return { JSXOpeningElement: (node) => {
7133
7133
  const tag = getElementType(node, context.settings);
7134
7134
  if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.has(tag)) return;
@@ -7247,7 +7247,7 @@ const isCreateElementCall = (node) => {
7247
7247
  //#region src/plugin/rules/react-builtins/button-has-type.ts
7248
7248
  const MISSING_MESSAGE$2 = "Your users can submit the form by accident because a `<button>` with no `type` defaults to submit.";
7249
7249
  const INVALID_MESSAGE = "This button has an invalid `type`, so the browser may treat it like a submit button.";
7250
- const resolveSettings$47 = (settings) => {
7250
+ const resolveSettings$48 = (settings) => {
7251
7251
  const reactDoctor = settings?.["react-doctor"];
7252
7252
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.buttonHasType ?? {} : {};
7253
7253
  return {
@@ -7485,7 +7485,7 @@ const buttonHasType = defineRule({
7485
7485
  severity: "warn",
7486
7486
  recommendation: "Set an explicit button `type` so plain buttons do not submit forms by accident: `type=\"button\"`, `\"submit\"`, or `\"reset\"`.",
7487
7487
  create: (context) => {
7488
- const settings = resolveSettings$47(context.settings);
7488
+ const settings = resolveSettings$48(context.settings);
7489
7489
  const isTestlikeFile = isTestlikeFilename(context.filename);
7490
7490
  return {
7491
7491
  JSXOpeningElement(node) {
@@ -7566,7 +7566,7 @@ const buttonHasType = defineRule({
7566
7566
  //#region src/plugin/rules/react-builtins/checked-requires-onchange-or-readonly.ts
7567
7567
  const MISSING_MESSAGE$1 = "Your users can't toggle this input because `checked` has no `onChange`.";
7568
7568
  const EXCLUSIVE_MESSAGE = "This input mixes `checked` with `defaultChecked`, so React can't tell whether it is controlled or uncontrolled.";
7569
- const resolveSettings$46 = (settings) => {
7569
+ const resolveSettings$47 = (settings) => {
7570
7570
  const reactDoctor = settings?.["react-doctor"];
7571
7571
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.checkedRequiresOnchangeOrReadonly ?? {} : {};
7572
7572
  return {
@@ -7667,7 +7667,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
7667
7667
  recommendation: "Add `onChange`, `readOnly`, or `defaultChecked` so React knows whether the checkbox is editable, display-only, or uncontrolled.",
7668
7668
  category: "Correctness",
7669
7669
  create: (context) => {
7670
- const settings = resolveSettings$46(context.settings);
7670
+ const settings = resolveSettings$47(context.settings);
7671
7671
  const reportFromPresence = (presence) => {
7672
7672
  if (presence.checkedNode && presence.defaultCheckedNode && !presence.bothCheckedAndDefaultForwarded && !settings.ignoreExclusiveCheckedAttribute) context.report({
7673
7673
  node: presence.checkedNode,
@@ -10380,7 +10380,7 @@ const WRAPPER_LABEL_PROP = "label";
10380
10380
  const SELECT_ELEMENT = "select";
10381
10381
  const DEFAULT_DEPTH = 5;
10382
10382
  const MAX_DEPTH = 25;
10383
- const resolveSettings$45 = (settings) => {
10383
+ const resolveSettings$46 = (settings) => {
10384
10384
  const reactDoctor = settings?.["react-doctor"];
10385
10385
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.controlHasAssociatedLabel ?? {} : {};
10386
10386
  return {
@@ -10713,7 +10713,7 @@ const controlHasAssociatedLabel = defineRule({
10713
10713
  recommendation: "Give every interactive control a label screen readers can read.",
10714
10714
  category: "Accessibility",
10715
10715
  create: (context) => {
10716
- const settings = resolveSettings$45(context.settings);
10716
+ const settings = resolveSettings$46(context.settings);
10717
10717
  const isTestlikeFile = isTestlikeFilename(context.filename);
10718
10718
  const iconComponentNames = /* @__PURE__ */ new Set();
10719
10719
  const iconNamespaceNames = /* @__PURE__ */ new Set();
@@ -12665,7 +12665,7 @@ const DEFAULT_ADDITIONAL_HOCS = [
12665
12665
  "lazy",
12666
12666
  "withTracking"
12667
12667
  ];
12668
- const resolveSettings$44 = (settings) => {
12668
+ const resolveSettings$45 = (settings) => {
12669
12669
  const reactDoctor = settings?.["react-doctor"];
12670
12670
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.displayName ?? {} : {};
12671
12671
  const additionalHoCs = new Set(ruleSettings.additionalHoCs ?? DEFAULT_ADDITIONAL_HOCS);
@@ -12847,7 +12847,7 @@ const displayName = defineRule({
12847
12847
  recommendation: "Give each component a `displayName` so DevTools shows a clear name.",
12848
12848
  category: "Architecture",
12849
12849
  create: (context) => {
12850
- const settings = resolveSettings$44(context.settings);
12850
+ const settings = resolveSettings$45(context.settings);
12851
12851
  const ignoreNamed = !settings.ignoreTranspilerName;
12852
12852
  const reportAt = (node) => {
12853
12853
  context.report({
@@ -19016,7 +19016,7 @@ const compileGlob = (pattern) => {
19016
19016
  };
19017
19017
  //#endregion
19018
19018
  //#region src/plugin/rules/react-builtins/forbid-component-props.ts
19019
- const resolveSettings$43 = (settings) => {
19019
+ const resolveSettings$44 = (settings) => {
19020
19020
  const reactDoctor = settings?.["react-doctor"];
19021
19021
  return { forbid: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidComponentProps ?? {} : {}).forbid ?? [] };
19022
19022
  };
@@ -19068,7 +19068,7 @@ const forbidComponentProps = defineRule({
19068
19068
  recommendation: "Configure blocked component props so callers cannot bypass the component API contract.",
19069
19069
  category: "Architecture",
19070
19070
  create: (context) => {
19071
- const entries = resolveSettings$43(context.settings).forbid.map(normalizeEntry);
19071
+ const entries = resolveSettings$44(context.settings).forbid.map(normalizeEntry);
19072
19072
  return { JSXOpeningElement(node) {
19073
19073
  if (entries.length === 0) return;
19074
19074
  if (!isSupportedJsxName(node.name)) return;
@@ -19097,7 +19097,7 @@ const forbidComponentProps = defineRule({
19097
19097
  //#endregion
19098
19098
  //#region src/plugin/rules/react-builtins/forbid-dom-props.ts
19099
19099
  const buildMessage$24 = (propName, customMessage) => customMessage ?? `Your project blocks the \`${propName}\` prop on plain HTML tags, so this bypasses the agreed DOM API contract.`;
19100
- const resolveSettings$42 = (settings) => {
19100
+ const resolveSettings$43 = (settings) => {
19101
19101
  const reactDoctor = settings?.["react-doctor"];
19102
19102
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
19103
19103
  const map = /* @__PURE__ */ new Map();
@@ -19119,7 +19119,7 @@ const forbidDomProps = defineRule({
19119
19119
  recommendation: "Configure blocked DOM props so plain HTML tags stay on the agreed DOM API surface.",
19120
19120
  category: "Architecture",
19121
19121
  create: (context) => {
19122
- const forbidMap = resolveSettings$42(context.settings);
19122
+ const forbidMap = resolveSettings$43(context.settings);
19123
19123
  return { JSXOpeningElement(node) {
19124
19124
  if (forbidMap.size === 0) return;
19125
19125
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -19182,7 +19182,7 @@ const isReactFunctionCall = (node, expectedCall) => {
19182
19182
  //#endregion
19183
19183
  //#region src/plugin/rules/react-builtins/forbid-elements.ts
19184
19184
  const buildMessage$23 = (element, customHelp) => customHelp ? `Your project blocks \`<${element}>\` here. ${customHelp}` : `Your project blocks \`<${element}>\` here, so code stays on the approved UI surface.`;
19185
- const resolveSettings$41 = (settings) => {
19185
+ const resolveSettings$42 = (settings) => {
19186
19186
  const reactDoctor = settings?.["react-doctor"];
19187
19187
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
19188
19188
  const map = /* @__PURE__ */ new Map();
@@ -19198,7 +19198,7 @@ const forbidElements = defineRule({
19198
19198
  recommendation: "Configure blocked elements so code stays on the approved UI primitives.",
19199
19199
  category: "Architecture",
19200
19200
  create: (context) => {
19201
- const forbidMap = resolveSettings$41(context.settings);
19201
+ const forbidMap = resolveSettings$42(context.settings);
19202
19202
  return {
19203
19203
  JSXOpeningElement(node) {
19204
19204
  if (forbidMap.size === 0) return;
@@ -19307,7 +19307,7 @@ const DEFAULT_HEADING_TAGS = [
19307
19307
  "h5",
19308
19308
  "h6"
19309
19309
  ];
19310
- const resolveSettings$40 = (settings) => {
19310
+ const resolveSettings$41 = (settings) => {
19311
19311
  const reactDoctor = settings?.["react-doctor"];
19312
19312
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
19313
19313
  return { headingTags: new Set([...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []]) };
@@ -19320,7 +19320,7 @@ const headingHasContent = defineRule({
19320
19320
  recommendation: "Put readable text in every heading.",
19321
19321
  category: "Accessibility",
19322
19322
  create: (context) => {
19323
- const settings = resolveSettings$40(context.settings);
19323
+ const settings = resolveSettings$41(context.settings);
19324
19324
  return { JSXOpeningElement(node) {
19325
19325
  const elementType = getElementType(node, context.settings);
19326
19326
  if (!settings.headingTags.has(elementType)) return;
@@ -19341,7 +19341,7 @@ const headingHasContent = defineRule({
19341
19341
  //#region src/plugin/rules/react-builtins/hook-use-state.ts
19342
19342
  const REQUIRE_DESTRUCTURE_MESSAGE = "`useState` should be destructured as `[value, setValue]` so readers can see the state value and setter together.";
19343
19343
  const NAMING_CONVENTION_MESSAGE = "This `useState` setter does not match its value name, so updates are harder to trace.";
19344
- const resolveSettings$39 = (settings) => {
19344
+ const resolveSettings$40 = (settings) => {
19345
19345
  const reactDoctor = settings?.["react-doctor"];
19346
19346
  return { allowDestructuredState: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.hookUseState ?? {} : {}).allowDestructuredState ?? false };
19347
19347
  };
@@ -19368,7 +19368,7 @@ const hookUseState = defineRule({
19368
19368
  recommendation: "Destructure `useState` as `const [thing, setThing] = useState(…)` so state reads and writes stay visible together.",
19369
19369
  category: "Architecture",
19370
19370
  create: (context) => {
19371
- const { allowDestructuredState } = resolveSettings$39(context.settings);
19371
+ const { allowDestructuredState } = resolveSettings$40(context.settings);
19372
19372
  return { CallExpression(node) {
19373
19373
  const callExpressionNode = node;
19374
19374
  if (!isReactFunctionCall(callExpressionNode, "useState")) return;
@@ -19501,7 +19501,7 @@ const hooksNoNanInDeps = defineRule({
19501
19501
  //#endregion
19502
19502
  //#region src/plugin/rules/a11y/html-has-lang.ts
19503
19503
  const MESSAGE$52 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
19504
- const resolveSettings$38 = (settings) => {
19504
+ const resolveSettings$39 = (settings) => {
19505
19505
  const reactDoctor = settings?.["react-doctor"];
19506
19506
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
19507
19507
  };
@@ -19538,7 +19538,7 @@ const htmlHasLang = defineRule({
19538
19538
  recommendation: "Set `<html lang=\"…\">` so screen readers know the page language.",
19539
19539
  category: "Accessibility",
19540
19540
  create: (context) => {
19541
- const settings = resolveSettings$38(context.settings);
19541
+ const settings = resolveSettings$39(context.settings);
19542
19542
  const tagSet = new Set(settings.htmlTags);
19543
19543
  return { JSXOpeningElement(node) {
19544
19544
  const tag = getElementType(node, context.settings);
@@ -20054,7 +20054,7 @@ const DEFAULT_REDUNDANT_WORDS = [
20054
20054
  "photo",
20055
20055
  "picture"
20056
20056
  ];
20057
- const resolveSettings$37 = (settings) => {
20057
+ const resolveSettings$38 = (settings) => {
20058
20058
  const reactDoctor = settings?.["react-doctor"];
20059
20059
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.imgRedundantAlt ?? {} : {};
20060
20060
  return {
@@ -20102,7 +20102,7 @@ const imgRedundantAlt = defineRule({
20102
20102
  category: "Accessibility",
20103
20103
  create: (context) => {
20104
20104
  if (isGeneratedImageRenderContext(context)) return {};
20105
- const settings = resolveSettings$37(context.settings);
20105
+ const settings = resolveSettings$38(context.settings);
20106
20106
  return { JSXOpeningElement(node) {
20107
20107
  const tag = getElementType(node, context.settings);
20108
20108
  if (!settings.components.includes(tag)) return;
@@ -20410,7 +20410,7 @@ const COMPOSITE_ITEM_ROLES = new Set([
20410
20410
  "row",
20411
20411
  "gridcell"
20412
20412
  ]);
20413
- const resolveSettings$36 = (settings) => {
20413
+ const resolveSettings$37 = (settings) => {
20414
20414
  const reactDoctor = settings?.["react-doctor"];
20415
20415
  return { tabbable: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.interactiveSupportsFocus ?? {} : {}).tabbable ?? DEFAULT_TABBABLE_ROLES };
20416
20416
  };
@@ -20422,7 +20422,7 @@ const interactiveSupportsFocus = defineRule({
20422
20422
  recommendation: "Add keyboard focus support so users can reach interactive elements without a pointer.",
20423
20423
  category: "Accessibility",
20424
20424
  create: (context) => {
20425
- const settings = resolveSettings$36(context.settings);
20425
+ const settings = resolveSettings$37(context.settings);
20426
20426
  const tabbableSet = new Set(settings.tabbable);
20427
20427
  return { JSXOpeningElement(node) {
20428
20428
  if (isLocalTestScaffoldJsx(node, context)) return;
@@ -23718,7 +23718,7 @@ const jsTosortedImmutable = defineRule({
23718
23718
  const NEVER_MESSAGE$2 = () => "This boolean prop style disagrees with the project setting, so equivalent true props are harder to scan consistently.";
23719
23719
  const ALWAYS_MESSAGE$2 = () => "This boolean prop style disagrees with the project setting, so equivalent true props are harder to scan consistently.";
23720
23720
  const FALSE_OMITTED_MESSAGE = (attributeName) => `\`${attributeName}={false}\` does nothing, so the explicit false value adds noise without changing output.`;
23721
- const resolveSettings$35 = (settings) => {
23721
+ const resolveSettings$36 = (settings) => {
23722
23722
  const reactDoctor = settings?.["react-doctor"];
23723
23723
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxBooleanValue ?? {} : {};
23724
23724
  return {
@@ -23736,7 +23736,7 @@ const jsxBooleanValue = defineRule({
23736
23736
  recommendation: "Use one boolean-attribute style so equivalent true props scan the same across the codebase.",
23737
23737
  category: "Architecture",
23738
23738
  create: (context) => {
23739
- const settings = resolveSettings$35(context.settings);
23739
+ const settings = resolveSettings$36(context.settings);
23740
23740
  const alwaysSet = new Set(settings.always);
23741
23741
  const neverSet = new Set(settings.never);
23742
23742
  return { JSXAttribute(node) {
@@ -23790,7 +23790,7 @@ const jsxBooleanValue = defineRule({
23790
23790
  const UNNECESSARY_BRACES_MESSAGE = "These curly braces wrap a literal value, so they add JSX noise without changing output.";
23791
23791
  const REQUIRED_BRACES_MESSAGE = "This JSX value needs `{ }` so React reads it as an expression instead of text.";
23792
23792
  const isAllowedMode = (value) => value === "always" || value === "never" || value === "ignore";
23793
- const resolveSettings$34 = (settings) => {
23793
+ const resolveSettings$35 = (settings) => {
23794
23794
  const reactDoctor = settings?.["react-doctor"];
23795
23795
  const ruleSettingsRaw = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxCurlyBracePresence : void 0;
23796
23796
  if (isAllowedMode(ruleSettingsRaw)) return {
@@ -23886,7 +23886,7 @@ const jsxCurlyBracePresence = defineRule({
23886
23886
  recommendation: "Use one JSX literal style so equivalent markup scans the same without extra JSX noise.",
23887
23887
  category: "Architecture",
23888
23888
  create: (context) => {
23889
- const settings = resolveSettings$34(context.settings);
23889
+ const settings = resolveSettings$35(context.settings);
23890
23890
  return {
23891
23891
  JSXAttribute(node) {
23892
23892
  const value = node.value;
@@ -23969,7 +23969,7 @@ const containsJsxElement = (root) => {
23969
23969
  //#region src/plugin/rules/react-builtins/jsx-filename-extension.ts
23970
23970
  const JSX_NOT_ALLOWED = (extension) => `This file contains JSX but uses a \`${extension}\` name, so the filename no longer signals JSX to readers or tooling conventions.`;
23971
23971
  const EXTENSION_ONLY_FOR_JSX = (extension) => `\`${extension}\` files are reserved for JSX here, so using that extension without JSX makes file-type conventions less useful.`;
23972
- const resolveSettings$33 = (settings) => {
23972
+ const resolveSettings$34 = (settings) => {
23973
23973
  const reactDoctor = settings?.["react-doctor"];
23974
23974
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFilenameExtension ?? {} : {};
23975
23975
  return {
@@ -23991,7 +23991,7 @@ const jsxFilenameExtension = defineRule({
23991
23991
  recommendation: "Name JSX files with the configured extension so file names accurately signal which files contain JSX.",
23992
23992
  category: "Architecture",
23993
23993
  create: (context) => {
23994
- const settings = resolveSettings$33(context.settings);
23994
+ const settings = resolveSettings$34(context.settings);
23995
23995
  const allowedExtensions = normalizeExtensions(settings.extensions);
23996
23996
  const filename = normalizeFilename(context.filename ?? "fixture.tsx");
23997
23997
  const extensionOnly = path.extname(filename).slice(1);
@@ -24032,7 +24032,7 @@ const jsxFilenameExtension = defineRule({
24032
24032
  //#region src/plugin/rules/react-builtins/jsx-fragments.ts
24033
24033
  const SYNTAX_MESSAGE = "`<React.Fragment>` is used where shorthand fragments are configured, so similar wrappers look different across the codebase.";
24034
24034
  const ELEMENT_MESSAGE = "Fragment shorthand is used where explicit fragments are configured, so similar wrappers look different across the codebase.";
24035
- const resolveSettings$32 = (settings) => {
24035
+ const resolveSettings$33 = (settings) => {
24036
24036
  const reactDoctor = settings?.["react-doctor"];
24037
24037
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFragments ?? {} : {}).mode ?? "syntax" };
24038
24038
  };
@@ -24044,7 +24044,7 @@ const jsxFragments = defineRule({
24044
24044
  recommendation: "Use one fragment style so identical wrappers do not look different across files.",
24045
24045
  category: "Architecture",
24046
24046
  create: (context) => {
24047
- const { mode } = resolveSettings$32(context.settings);
24047
+ const { mode } = resolveSettings$33(context.settings);
24048
24048
  return {
24049
24049
  JSXElement(node) {
24050
24050
  if (mode !== "syntax") return;
@@ -24088,7 +24088,7 @@ const buildPropRegex = (handlerPropPrefix) => {
24088
24088
  const escaped = tokens.map((token) => token.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("|");
24089
24089
  return new RegExp(`^(${escaped})[A-Z].*$`);
24090
24090
  };
24091
- const resolveSettings$31 = (settings) => {
24091
+ const resolveSettings$32 = (settings) => {
24092
24092
  const reactDoctor = settings?.["react-doctor"];
24093
24093
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxHandlerNames ?? {} : {};
24094
24094
  let handlerPrefix = DEFAULT_HANDLER_PREFIX;
@@ -24164,7 +24164,7 @@ const jsxHandlerNames = defineRule({
24164
24164
  recommendation: "Use the `on…` prefix for event-handler props and `handle…` for handlers so readers can trace event flow.",
24165
24165
  category: "Architecture",
24166
24166
  create: (context) => {
24167
- const settings = resolveSettings$31(context.settings);
24167
+ const settings = resolveSettings$32(context.settings);
24168
24168
  return { JSXAttribute(node) {
24169
24169
  if (settings.ignoreComponentNames.length > 0) {
24170
24170
  const opening = node.parent;
@@ -24239,19 +24239,6 @@ const hasJsxKeyAttribute = (openingElement) => {
24239
24239
  return false;
24240
24240
  };
24241
24241
  //#endregion
24242
- //#region src/plugin/utils/is-component-function.ts
24243
- const isFunctionAssignedToComponent = (functionNode) => {
24244
- let cursor = functionNode.parent ?? null;
24245
- while (isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
24246
- if (isNodeOfType(cursor, "VariableDeclarator") && isNodeOfType(cursor.id, "Identifier") && isUppercaseName(cursor.id.name)) return true;
24247
- return isNodeOfType(cursor, "ExportDefaultDeclaration");
24248
- };
24249
- const isComponentFunction$1 = (node) => {
24250
- if (!isFunctionLike$1(node)) return false;
24251
- if (isNodeOfType(node, "FunctionDeclaration")) return !node.id || node.id.name === "default" || isUppercaseName(node.id.name) || isNodeOfType(node.parent, "ExportDefaultDeclaration");
24252
- return isFunctionAssignedToComponent(node);
24253
- };
24254
- //#endregion
24255
24242
  //#region src/plugin/utils/is-const-declared-binding.ts
24256
24243
  const BINDING_PATTERN_ANCESTOR_TYPES = new Set([
24257
24244
  "VariableDeclarator",
@@ -24296,17 +24283,11 @@ const ITERATOR_METHOD_NAMES$1 = new Set([
24296
24283
  "flatMap",
24297
24284
  "from"
24298
24285
  ]);
24299
- const RENDERING_CALL_NAMES = new Set([
24300
- "createPortal",
24301
- "hydrate",
24302
- "hydrateRoot",
24303
- "render"
24304
- ]);
24305
24286
  const MISSING_KEY_ARRAY = "Your users can see the wrong data when this array reorders.";
24306
24287
  const MISSING_KEY_ITERATOR = "Your users can see the wrong data when this list reorders.";
24307
- const KEY_BEFORE_SPREAD = "Place this `key` after the `{...spread}` so the spread cannot override it.";
24288
+ const KEY_AFTER_SPREAD = "Place this `key` before the `{...spread}` so the JSX transform can always extract it.";
24308
24289
  const DUPLICATE_KEY = (keyValue) => `Your users can see the wrong data because two elements share the key "${keyValue}".`;
24309
- const resolveSettings$30 = (settings) => {
24290
+ const resolveSettings$31 = (settings) => {
24310
24291
  const reactDoctor = settings?.["react-doctor"];
24311
24292
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxKey ?? {} : {};
24312
24293
  return {
@@ -24320,33 +24301,6 @@ const findArrayVariableDeclarator = (arrayExpression) => {
24320
24301
  if (ancestor && isNodeOfType(ancestor, "VariableDeclarator") && ancestor.init === wrapped && isNodeOfType(ancestor.id, "Identifier")) return ancestor;
24321
24302
  return null;
24322
24303
  };
24323
- const isArrayNestedInObjectProperty = (arrayExpression) => {
24324
- let current = findTransparentExpressionRoot(arrayExpression);
24325
- while (current.parent) {
24326
- const parent = current.parent;
24327
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === current || parent.alternate === current) || isNodeOfType(parent, "LogicalExpression") && (parent.left === current || parent.right === current)) {
24328
- current = findTransparentExpressionRoot(parent);
24329
- continue;
24330
- }
24331
- return isNodeOfType(parent, "Property") && parent.value === current;
24332
- }
24333
- return false;
24334
- };
24335
- const isArrayPassedToNonRenderingCall = (arrayExpression, scopes) => {
24336
- const transparentArray = findTransparentExpressionRoot(arrayExpression);
24337
- const callExpression = transparentArray.parent;
24338
- if (!callExpression || !isNodeOfType(callExpression, "CallExpression") || !callExpression.arguments.some((argument) => argument === transparentArray)) return false;
24339
- const argumentIndex = callExpression.arguments.findIndex((argument) => argument === transparentArray);
24340
- if (isReactApiCall(callExpression, "createElement", scopes, {
24341
- allowGlobalReactNamespace: true,
24342
- allowUnboundBareCalls: true,
24343
- resolveNamedAliases: true
24344
- })) return argumentIndex < 2;
24345
- const callee = stripParenExpression(callExpression.callee);
24346
- if (isNodeOfType(callee, "Identifier")) return !RENDERING_CALL_NAMES.has(callee.name);
24347
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return !RENDERING_CALL_NAMES.has(callee.property.name);
24348
- return true;
24349
- };
24350
24304
  const isIdentityIteratorCallback = (callback) => {
24351
24305
  const callbackFunction = stripParenExpression(callback);
24352
24306
  if (!isNodeOfType(callbackFunction, "ArrowFunctionExpression") && !isNodeOfType(callbackFunction, "FunctionExpression")) return true;
@@ -24443,7 +24397,7 @@ const findNamedCallbackIteratorCall = (functionNode) => {
24443
24397
  namedCallbackIteratorCallCache.set(functionNode, iteratorCall);
24444
24398
  return iteratorCall;
24445
24399
  };
24446
- const findEnclosingIteratorContext = (jsxNode, scopes) => {
24400
+ const findEnclosingIteratorContext = (jsxNode) => {
24447
24401
  let current = jsxNode;
24448
24402
  let isOutsideContainingFunction = false;
24449
24403
  let didSeeReturnStatement = false;
@@ -24465,8 +24419,6 @@ const findEnclosingIteratorContext = (jsxNode, scopes) => {
24465
24419
  isOutsideContainingFunction = true;
24466
24420
  } else if (isNodeOfType(parent, "ArrayExpression")) {
24467
24421
  if (isOutsideContainingFunction) return null;
24468
- if (isArrayNestedInObjectProperty(parent)) return null;
24469
- if (isArrayPassedToNonRenderingCall(parent, scopes)) return null;
24470
24422
  const arrayParent = parent.parent;
24471
24423
  if (arrayParent && isNodeOfType(arrayParent, "Property")) return null;
24472
24424
  if (arrayParent && isNodeOfType(arrayParent, "ArrayExpression")) return null;
@@ -24548,15 +24500,9 @@ const isWithinChildrenToArray = (jsxNode) => {
24548
24500
  const isKeyPropertyName = (propertyKey) => {
24549
24501
  if (isNodeOfType(propertyKey, "Identifier")) return propertyKey.name === "key";
24550
24502
  if (isNodeOfType(propertyKey, "Literal")) return String(propertyKey.value) === "key";
24551
- return false;
24503
+ return true;
24552
24504
  };
24553
24505
  const isThisPropsMember = (expression) => isNodeOfType(expression, "MemberExpression") && !expression.computed && isNodeOfType(expression.object, "ThisExpression") && isNodeOfType(expression.property, "Identifier") && expression.property.name === "props";
24554
- const isDirectComponentPropsParameter = (bindingIdentifier, functionNode) => {
24555
- if (!isFunctionLike$1(functionNode) || !isComponentFunction$1(functionNode)) return false;
24556
- const firstParameter = functionNode.params?.[0];
24557
- if (!firstParameter) return false;
24558
- return (isNodeOfType(firstParameter, "AssignmentPattern") ? firstParameter.left : firstParameter) === bindingIdentifier;
24559
- };
24560
24506
  const isFunctionParameterPropsRest = (bindingIdentifier) => {
24561
24507
  const restElement = bindingIdentifier.parent;
24562
24508
  if (!restElement || !isNodeOfType(restElement, "RestElement")) return false;
@@ -24566,7 +24512,7 @@ const isFunctionParameterPropsRest = (bindingIdentifier) => {
24566
24512
  const parameterNode = patternParent && isNodeOfType(patternParent, "AssignmentPattern") && patternParent.left === objectPattern ? patternParent : objectPattern;
24567
24513
  const functionNode = parameterNode.parent;
24568
24514
  if (!functionNode || !isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "FunctionDeclaration")) return false;
24569
- return isComponentFunction$1(functionNode) && functionNode.params[0] === parameterNode;
24515
+ return functionNode.params.some((param) => param === parameterNode);
24570
24516
  };
24571
24517
  const isRestBindingWithKeyExtracted = (bindingIdentifier) => {
24572
24518
  const restElement = bindingIdentifier.parent;
@@ -24579,14 +24525,14 @@ const isRestBindingWithKeyExtracted = (bindingIdentifier) => {
24579
24525
  }
24580
24526
  return false;
24581
24527
  };
24582
- const isBindingAssignedKey = (scopeOwner, bindingName) => {
24583
- let didFindKeyAssignment = false;
24528
+ const isBindingObjectMutated = (scopeOwner, bindingName) => {
24529
+ let didFindMutation = false;
24584
24530
  walkAst(scopeOwner, (node) => {
24585
- if (didFindKeyAssignment) return false;
24531
+ if (didFindMutation) return false;
24586
24532
  if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression")) {
24587
24533
  const assignedObject = stripParenExpression(node.left.object);
24588
- if (isNodeOfType(assignedObject, "Identifier") && assignedObject.name === bindingName && isKeyPropertyName(node.left.property)) {
24589
- didFindKeyAssignment = true;
24534
+ if (isNodeOfType(assignedObject, "Identifier") && assignedObject.name === bindingName) {
24535
+ didFindMutation = true;
24590
24536
  return false;
24591
24537
  }
24592
24538
  }
@@ -24594,63 +24540,59 @@ const isBindingAssignedKey = (scopeOwner, bindingName) => {
24594
24540
  const callee = node.callee;
24595
24541
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Object" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "assign") {
24596
24542
  const firstArgument = node.arguments[0];
24597
- if (firstArgument && isNodeOfType(firstArgument, "Identifier") && firstArgument.name === bindingName) for (const sourceArgument of node.arguments.slice(1)) {
24598
- const source = stripParenExpression(sourceArgument);
24599
- if (!isNodeOfType(source, "ObjectExpression")) continue;
24600
- if (source.properties.some((property) => isNodeOfType(property, "Property") && isKeyPropertyName(property.key))) {
24601
- didFindKeyAssignment = true;
24602
- return false;
24603
- }
24543
+ if (firstArgument && isNodeOfType(firstArgument, "Identifier") && firstArgument.name === bindingName) {
24544
+ didFindMutation = true;
24545
+ return false;
24604
24546
  }
24605
24547
  }
24606
24548
  }
24607
24549
  });
24608
- return didFindKeyAssignment;
24550
+ return didFindMutation;
24609
24551
  };
24610
- const spreadExpressionHasKey = (expression, depth) => {
24552
+ const spreadExpressionCanCarryKey = (expression, depth) => {
24611
24553
  const inner = stripParenExpression(expression);
24612
24554
  if (isNodeOfType(inner, "ObjectExpression")) {
24613
24555
  for (const property of inner.properties) {
24614
24556
  if (isNodeOfType(property, "SpreadElement")) {
24615
- if (depth >= 3) continue;
24616
- if (spreadExpressionHasKey(property.argument, depth + 1)) return true;
24557
+ if (depth >= 3) return true;
24558
+ if (spreadExpressionCanCarryKey(property.argument, depth + 1)) return true;
24617
24559
  continue;
24618
24560
  }
24619
- if (!isNodeOfType(property, "Property")) continue;
24561
+ if (!isNodeOfType(property, "Property")) return true;
24562
+ if (property.computed) return true;
24620
24563
  if (isKeyPropertyName(property.key)) return true;
24621
24564
  }
24622
24565
  return false;
24623
24566
  }
24624
24567
  if (isNodeOfType(inner, "ConditionalExpression")) {
24625
- if (depth >= 3) return false;
24626
- return spreadExpressionHasKey(inner.consequent, depth + 1) || spreadExpressionHasKey(inner.alternate, depth + 1);
24568
+ if (depth >= 3) return true;
24569
+ return spreadExpressionCanCarryKey(inner.consequent, depth + 1) || spreadExpressionCanCarryKey(inner.alternate, depth + 1);
24627
24570
  }
24628
24571
  if (isNodeOfType(inner, "LogicalExpression")) {
24629
- if (depth >= 3) return false;
24630
- if (inner.operator === "&&") return spreadExpressionHasKey(inner.right, depth + 1);
24631
- return spreadExpressionHasKey(inner.left, depth + 1) || spreadExpressionHasKey(inner.right, depth + 1);
24572
+ if (depth >= 3) return true;
24573
+ if (inner.operator === "&&") return spreadExpressionCanCarryKey(inner.right, depth + 1);
24574
+ return spreadExpressionCanCarryKey(inner.left, depth + 1) || spreadExpressionCanCarryKey(inner.right, depth + 1);
24632
24575
  }
24633
24576
  if (isNodeOfType(inner, "Literal")) return false;
24634
24577
  if (isThisPropsMember(inner)) return false;
24635
24578
  if (isNodeOfType(inner, "Identifier")) {
24636
24579
  if (inner.name === "undefined") return false;
24637
- if (depth >= 3) return false;
24580
+ if (depth >= 3) return true;
24638
24581
  const binding = findVariableInitializer(inner, inner.name);
24639
- if (!binding) return false;
24582
+ if (!binding) return true;
24640
24583
  if (isRestBindingWithKeyExtracted(binding.bindingIdentifier)) return false;
24641
24584
  if (isFunctionParameterPropsRest(binding.bindingIdentifier)) return false;
24642
- if (isDirectComponentPropsParameter(binding.bindingIdentifier, binding.scopeOwner)) return false;
24643
- if (!isConstDeclaredBinding(binding) || !binding.initializer) return false;
24644
- if (isBindingAssignedKey(binding.scopeOwner, inner.name)) return true;
24645
- return spreadExpressionHasKey(binding.initializer, depth + 1);
24585
+ if (!isConstDeclaredBinding(binding) || !binding.initializer) return true;
24586
+ if (isBindingObjectMutated(binding.scopeOwner, inner.name)) return true;
24587
+ return spreadExpressionCanCarryKey(binding.initializer, depth + 1);
24646
24588
  }
24647
- return false;
24589
+ return true;
24648
24590
  };
24649
- const spreadCanOverwriteKey = (spreadAttribute) => spreadExpressionHasKey(spreadAttribute.argument, 0);
24591
+ const spreadCanOverwriteKey = (spreadAttribute) => spreadExpressionCanCarryKey(spreadAttribute.argument, 0);
24650
24592
  const checkKeyBeforeSpread = (context, openingElement) => {
24651
24593
  let keyIndex = null;
24652
24594
  let keyAttribute = null;
24653
- let lastKeyCarryingSpreadIndex = null;
24595
+ let firstKeyCarryingSpreadIndex = null;
24654
24596
  for (let attributeIndex = 0; attributeIndex < openingElement.attributes.length; attributeIndex++) {
24655
24597
  const attribute = openingElement.attributes[attributeIndex];
24656
24598
  if (isNodeOfType(attribute, "JSXAttribute")) {
@@ -24658,11 +24600,11 @@ const checkKeyBeforeSpread = (context, openingElement) => {
24658
24600
  keyIndex = attributeIndex;
24659
24601
  keyAttribute = attribute;
24660
24602
  }
24661
- } else if (isNodeOfType(attribute, "JSXSpreadAttribute") && spreadCanOverwriteKey(attribute)) lastKeyCarryingSpreadIndex = attributeIndex;
24603
+ } else if (isNodeOfType(attribute, "JSXSpreadAttribute") && firstKeyCarryingSpreadIndex === null && spreadCanOverwriteKey(attribute)) firstKeyCarryingSpreadIndex = attributeIndex;
24662
24604
  }
24663
- if (keyIndex !== null && lastKeyCarryingSpreadIndex !== null && lastKeyCarryingSpreadIndex > keyIndex && keyAttribute) context.report({
24605
+ if (keyIndex !== null && firstKeyCarryingSpreadIndex !== null && firstKeyCarryingSpreadIndex < keyIndex && keyAttribute) context.report({
24664
24606
  node: keyAttribute,
24665
- message: KEY_BEFORE_SPREAD
24607
+ message: KEY_AFTER_SPREAD
24666
24608
  });
24667
24609
  };
24668
24610
  const getKeyAttributeValueString = (openingElement) => {
@@ -24706,7 +24648,7 @@ const jsxKey = defineRule({
24706
24648
  severity: "error",
24707
24649
  recommendation: "Add a stable `key` prop so React can keep list items matched to the right data when the list changes.",
24708
24650
  create: (context) => {
24709
- const settings = resolveSettings$30(context.settings);
24651
+ const settings = resolveSettings$31(context.settings);
24710
24652
  return {
24711
24653
  JSXElement(node) {
24712
24654
  const openingElement = node.openingElement;
@@ -24724,7 +24666,7 @@ const jsxKey = defineRule({
24724
24666
  else seenKeys.add(keyValue.keyValue);
24725
24667
  }
24726
24668
  }
24727
- const enclosingContext = findEnclosingIteratorContext(node, context.scopes);
24669
+ const enclosingContext = findEnclosingIteratorContext(node);
24728
24670
  if (!enclosingContext) return;
24729
24671
  if (isWithinChildrenToArray(node)) return;
24730
24672
  if (hasJsxKeyAttribute(openingElement)) return;
@@ -24760,7 +24702,7 @@ const jsxKey = defineRule({
24760
24702
  //#region src/plugin/rules/react-builtins/jsx-max-depth.ts
24761
24703
  const buildMessage$19 = (depth, max) => `This JSX is hard to read at ${depth} levels deep, past the limit of ${max}.`;
24762
24704
  const DEFAULT_MAX_DEPTH = 14;
24763
- const resolveSettings$29 = (settings) => {
24705
+ const resolveSettings$30 = (settings) => {
24764
24706
  const reactDoctor = settings?.["react-doctor"];
24765
24707
  return { max: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxMaxDepth ?? {} : {}).max ?? DEFAULT_MAX_DEPTH };
24766
24708
  };
@@ -24829,7 +24771,7 @@ const jsxMaxDepth = defineRule({
24829
24771
  recommendation: "Pull deeply nested JSX into smaller components so it's easier to read.",
24830
24772
  category: "Architecture",
24831
24773
  create: (context) => {
24832
- const { max } = resolveSettings$29(context.settings);
24774
+ const { max } = resolveSettings$30(context.settings);
24833
24775
  const deepestLeafPerTree = /* @__PURE__ */ new Map();
24834
24776
  const checkNode = (node) => {
24835
24777
  if (!isLeafJsxNode(node)) return;
@@ -26801,7 +26743,7 @@ const jsxNoNewObjectAsProp = defineRule({
26801
26743
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
26802
26744
  const MESSAGE$43 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
26803
26745
  const JAVASCRIPT_URL_PATTERN = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
26804
- const resolveSettings$28 = (settings) => {
26746
+ const resolveSettings$29 = (settings) => {
26805
26747
  const reactDoctor = settings?.["react-doctor"];
26806
26748
  if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
26807
26749
  return reactDoctor.jsxNoScriptUrl ?? {};
@@ -26823,7 +26765,7 @@ const jsxNoScriptUrl = defineRule({
26823
26765
  recommendation: "Use real event handlers instead of `javascript:` URLs so injected URL text cannot execute as code.",
26824
26766
  category: "Security",
26825
26767
  create: (context) => {
26826
- const options = resolveSettings$28(context.settings);
26768
+ const options = resolveSettings$29(context.settings);
26827
26769
  return { JSXOpeningElement(node) {
26828
26770
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
26829
26771
  const elementName = resolveJsxElementType(node);
@@ -26844,6 +26786,283 @@ const jsxNoScriptUrl = defineRule({
26844
26786
  }
26845
26787
  });
26846
26788
  //#endregion
26789
+ //#region src/plugin/rules/react-builtins/jsx-no-target-blank.ts
26790
+ const NOREFERRER_MESSAGE = "`target=\"_blank\"` without `rel=\"noreferrer\"` lets the linked page hijack your tab to a phishing site.";
26791
+ const NOOPENER_MESSAGE = "`target=\"_blank\"` without `rel` lets the linked page hijack your tab to a phishing site.";
26792
+ const SPREAD_MESSAGE = "A spread here can add `target=\"_blank\"`, letting the linked page hijack your tab to a phishing site.";
26793
+ const resolveSettings$28 = (settings) => {
26794
+ const reactDoctor = settings?.["react-doctor"];
26795
+ const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxNoTargetBlank ?? {} : {};
26796
+ const reactSettings = typeof settings?.react === "object" && settings.react !== null ? settings.react : {};
26797
+ const linkComponents = /* @__PURE__ */ new Map();
26798
+ for (const entry of reactSettings.linkComponents ?? []) if (typeof entry === "string") linkComponents.set(entry, ["href"]);
26799
+ else if (typeof entry === "object" && entry !== null) {
26800
+ const linkAttribute = entry.linkAttribute ?? "href";
26801
+ linkComponents.set(entry.name, Array.isArray(linkAttribute) ? linkAttribute : [linkAttribute]);
26802
+ }
26803
+ const formComponents = /* @__PURE__ */ new Map();
26804
+ for (const entry of reactSettings.formComponents ?? []) if (typeof entry === "string") formComponents.set(entry, ["action"]);
26805
+ else if (typeof entry === "object" && entry !== null) {
26806
+ const formAttribute = entry.formAttribute ?? "action";
26807
+ formComponents.set(entry.name, Array.isArray(formAttribute) ? formAttribute : [formAttribute]);
26808
+ }
26809
+ return {
26810
+ enforceDynamicLinks: ruleSettings.enforceDynamicLinks ?? "always",
26811
+ warnOnSpreadAttributes: ruleSettings.warnOnSpreadAttributes ?? false,
26812
+ allowReferrer: ruleSettings.allowReferrer ?? false,
26813
+ links: ruleSettings.links ?? true,
26814
+ forms: ruleSettings.forms ?? false,
26815
+ linkComponents,
26816
+ formComponents
26817
+ };
26818
+ };
26819
+ const isExternalLink = (href) => href.includes("//");
26820
+ const matchHrefExpression = (expression, state) => {
26821
+ if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") {
26822
+ if (isExternalLink(expression.value)) state.isExternal = true;
26823
+ return;
26824
+ }
26825
+ if (isNodeOfType(expression, "Identifier")) {
26826
+ state.isDynamic = true;
26827
+ return;
26828
+ }
26829
+ if (isNodeOfType(expression, "ConditionalExpression")) {
26830
+ matchHrefExpression(expression.consequent, state);
26831
+ matchHrefExpression(expression.alternate, state);
26832
+ }
26833
+ };
26834
+ const checkHref = (attributeValue, enforceDynamicLinks) => {
26835
+ const state = {
26836
+ isExternal: false,
26837
+ isDynamic: false
26838
+ };
26839
+ if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") state.isExternal = isExternalLink(attributeValue.value);
26840
+ else if (isNodeOfType(attributeValue, "JSXExpressionContainer")) matchHrefExpression(attributeValue.expression, state);
26841
+ if (enforceDynamicLinks === "never") return !state.isExternal || state.isDynamic;
26842
+ return !(state.isExternal || state.isDynamic);
26843
+ };
26844
+ const checkRelValue = (text, allowReferrer) => {
26845
+ const tokens = text.split(/\s+/);
26846
+ if (allowReferrer) return tokens.includes("noopener") || tokens.includes("noreferrer");
26847
+ return tokens.some((token) => token.toLowerCase() === "noreferrer");
26848
+ };
26849
+ const matchRelExpression = (expression, allowReferrer) => {
26850
+ const empty = {
26851
+ combined: false,
26852
+ testName: "",
26853
+ consequent: false,
26854
+ alternate: false
26855
+ };
26856
+ if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return {
26857
+ combined: checkRelValue(expression.value, allowReferrer),
26858
+ testName: "",
26859
+ consequent: false,
26860
+ alternate: false
26861
+ };
26862
+ if (isNodeOfType(expression, "ConditionalExpression")) {
26863
+ const consequent = matchRelExpression(expression.consequent, allowReferrer);
26864
+ const alternate = matchRelExpression(expression.alternate, allowReferrer);
26865
+ const test = expression.test;
26866
+ if (isNodeOfType(test, "Identifier")) return {
26867
+ combined: consequent.combined && alternate.combined,
26868
+ testName: test.name,
26869
+ consequent: consequent.combined,
26870
+ alternate: alternate.combined
26871
+ };
26872
+ return {
26873
+ combined: consequent.combined && alternate.combined,
26874
+ testName: "",
26875
+ consequent: consequent.combined,
26876
+ alternate: alternate.combined
26877
+ };
26878
+ }
26879
+ return empty;
26880
+ };
26881
+ const checkRel = (attributeValue, allowReferrer) => {
26882
+ const empty = {
26883
+ combined: false,
26884
+ testName: "",
26885
+ consequent: false,
26886
+ alternate: false
26887
+ };
26888
+ if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return {
26889
+ combined: checkRelValue(attributeValue.value, allowReferrer),
26890
+ testName: "",
26891
+ consequent: false,
26892
+ alternate: false
26893
+ };
26894
+ if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
26895
+ const expression = attributeValue.expression;
26896
+ if (expression.type === "JSXEmptyExpression") return empty;
26897
+ return matchRelExpression(expression, allowReferrer);
26898
+ }
26899
+ return empty;
26900
+ };
26901
+ const matchTargetExpression = (expression) => {
26902
+ const empty = {
26903
+ combined: false,
26904
+ testName: "",
26905
+ consequent: false,
26906
+ alternate: false
26907
+ };
26908
+ if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return {
26909
+ combined: expression.value.toLowerCase() === "_blank",
26910
+ testName: "",
26911
+ consequent: false,
26912
+ alternate: false
26913
+ };
26914
+ if (isNodeOfType(expression, "ConditionalExpression")) {
26915
+ const consequent = matchTargetExpression(expression.consequent);
26916
+ const alternate = matchTargetExpression(expression.alternate);
26917
+ const test = expression.test;
26918
+ const combined = consequent.combined || alternate.combined;
26919
+ if (isNodeOfType(test, "Identifier")) return {
26920
+ combined,
26921
+ testName: test.name,
26922
+ consequent: consequent.combined,
26923
+ alternate: alternate.combined
26924
+ };
26925
+ return {
26926
+ combined,
26927
+ testName: "",
26928
+ consequent: consequent.combined,
26929
+ alternate: alternate.combined
26930
+ };
26931
+ }
26932
+ return empty;
26933
+ };
26934
+ const checkTarget = (attributeValue) => {
26935
+ if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return {
26936
+ combined: attributeValue.value.toLowerCase() === "_blank",
26937
+ testName: "",
26938
+ consequent: false,
26939
+ alternate: false
26940
+ };
26941
+ if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
26942
+ const expression = attributeValue.expression;
26943
+ if (expression.type === "JSXEmptyExpression") return {
26944
+ combined: false,
26945
+ testName: "",
26946
+ consequent: false,
26947
+ alternate: false
26948
+ };
26949
+ return matchTargetExpression(expression);
26950
+ }
26951
+ return {
26952
+ combined: false,
26953
+ testName: "",
26954
+ consequent: false,
26955
+ alternate: false
26956
+ };
26957
+ };
26958
+ const jsxNoTargetBlank = defineRule({
26959
+ id: "jsx-no-target-blank",
26960
+ title: "Unsafe target=_blank link",
26961
+ severity: "warn",
26962
+ recommendation: "Add `rel=\"noreferrer\"` (or `\"noopener\"`) when using `target=\"_blank\"`.",
26963
+ category: "Security",
26964
+ create: (context) => {
26965
+ const settings = resolveSettings$28(context.settings);
26966
+ const isLink = (tagName) => {
26967
+ if (!settings.links) return false;
26968
+ if (tagName === "a") return true;
26969
+ return settings.linkComponents.has(tagName);
26970
+ };
26971
+ const isForm = (tagName) => {
26972
+ if (!settings.forms) return false;
26973
+ if (tagName === "form") return true;
26974
+ return settings.formComponents.has(tagName);
26975
+ };
26976
+ return { JSXOpeningElement(node) {
26977
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
26978
+ const tagName = resolveJsxElementType(node);
26979
+ if (!tagName) return;
26980
+ if (!isLink(tagName) && !isForm(tagName)) return;
26981
+ const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
26982
+ const formAttributeNames = settings.formComponents.get(tagName) ?? ["action"];
26983
+ let targetTuple = {
26984
+ combined: false,
26985
+ testName: "",
26986
+ consequent: false,
26987
+ alternate: false
26988
+ };
26989
+ let relTuple = {
26990
+ combined: false,
26991
+ testName: "",
26992
+ consequent: false,
26993
+ alternate: false
26994
+ };
26995
+ let isHrefValid = true;
26996
+ let hasHrefValue = false;
26997
+ let warnSpread = false;
26998
+ let targetReportNode = node.name;
26999
+ let spreadReportNode = null;
27000
+ for (const attribute of node.attributes) {
27001
+ if (isNodeOfType(attribute, "JSXSpreadAttribute")) {
27002
+ if (settings.warnOnSpreadAttributes) {
27003
+ warnSpread = true;
27004
+ spreadReportNode = attribute;
27005
+ targetTuple = {
27006
+ combined: false,
27007
+ testName: "",
27008
+ consequent: false,
27009
+ alternate: false
27010
+ };
27011
+ relTuple = {
27012
+ combined: false,
27013
+ testName: "",
27014
+ consequent: false,
27015
+ alternate: false
27016
+ };
27017
+ isHrefValid = false;
27018
+ hasHrefValue = true;
27019
+ }
27020
+ continue;
27021
+ }
27022
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
27023
+ const attributeName = attribute.name;
27024
+ if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
27025
+ const propName = attributeName.name;
27026
+ const value = attribute.value;
27027
+ if (propName === "target") {
27028
+ if (value) {
27029
+ targetTuple = checkTarget(value);
27030
+ targetReportNode = value;
27031
+ }
27032
+ } else if (propName === "href" || propName === "action" || linkAttributeNames.includes(propName) || formAttributeNames.includes(propName)) {
27033
+ if (value) {
27034
+ hasHrefValue = true;
27035
+ isHrefValid = checkHref(value, settings.enforceDynamicLinks);
27036
+ }
27037
+ } else if (propName === "rel" && value) relTuple = checkRel(value, settings.allowReferrer);
27038
+ }
27039
+ if (warnSpread) {
27040
+ if (hasHrefValue && isHrefValid || relTuple.combined) return;
27041
+ context.report({
27042
+ node: spreadReportNode ?? node,
27043
+ message: SPREAD_MESSAGE
27044
+ });
27045
+ return;
27046
+ }
27047
+ if (!isHrefValid) {
27048
+ if (targetTuple.testName !== "" && targetTuple.testName === relTuple.testName) {
27049
+ const consequentBad = targetTuple.consequent && !relTuple.consequent;
27050
+ const alternateBad = targetTuple.alternate && !relTuple.alternate;
27051
+ if (consequentBad || alternateBad) context.report({
27052
+ node: targetReportNode,
27053
+ message: settings.allowReferrer ? NOOPENER_MESSAGE : NOREFERRER_MESSAGE
27054
+ });
27055
+ return;
27056
+ }
27057
+ if (targetTuple.combined && !relTuple.combined) context.report({
27058
+ node: targetReportNode,
27059
+ message: settings.allowReferrer ? NOOPENER_MESSAGE : NOREFERRER_MESSAGE
27060
+ });
27061
+ }
27062
+ } };
27063
+ }
27064
+ });
27065
+ //#endregion
26847
27066
  //#region src/plugin/rules/react-builtins/jsx-no-undef.ts
26848
27067
  const buildMessage$18 = (name) => `\`${name}\` crashes at runtime because it isn't defined here.`;
26849
27068
  const KNOWN_GLOBALS = new Set([
@@ -28501,30 +28720,6 @@ const hasAncestorLayoutMatching = (pageFilename, matchesLayout) => {
28501
28720
  const hasAncestorMetadataLayout = (pageFilename) => hasAncestorLayoutMatching(pageFilename, (layoutPath) => METADATA_EXPORT_NAMES.some((exportName) => doesModuleExportName(layoutPath, exportName)));
28502
28721
  //#endregion
28503
28722
  //#region src/plugin/rules/nextjs/nextjs-missing-metadata.ts
28504
- const unwrapAwaitedExpression = (node) => {
28505
- let expression = stripParenExpression(node);
28506
- while (isNodeOfType(expression, "AwaitExpression")) expression = stripParenExpression(expression.argument);
28507
- return expression;
28508
- };
28509
- const isNextNavigationRedirectCall = (node, context) => {
28510
- const expression = unwrapAwaitedExpression(node);
28511
- if (!isNodeOfType(expression, "CallExpression")) return false;
28512
- const callee = stripParenExpression(expression.callee);
28513
- if (!isNodeOfType(callee, "Identifier")) return false;
28514
- if (context.scopes.symbolFor(callee)?.kind !== "import") return false;
28515
- const importedName = getImportedNameFromModule(expression, callee.name, "next/navigation");
28516
- return importedName === "redirect" || importedName === "permanentRedirect";
28517
- };
28518
- const isRedirectOnlyDefaultExport = (programNode, context) => {
28519
- const defaultExport = findExportedValue(programNode, "default");
28520
- if (!defaultExport || !isFunctionLike$1(defaultExport)) return false;
28521
- if (!isNodeOfType(defaultExport.body, "BlockStatement")) return isNextNavigationRedirectCall(defaultExport.body, context);
28522
- if (defaultExport.body.body.length !== 1) return false;
28523
- const onlyStatement = defaultExport.body.body[0];
28524
- if (isNodeOfType(onlyStatement, "ExpressionStatement")) return isNextNavigationRedirectCall(onlyStatement.expression, context);
28525
- if (isNodeOfType(onlyStatement, "ReturnStatement") && onlyStatement.argument) return isNextNavigationRedirectCall(onlyStatement.argument, context);
28526
- return false;
28527
- };
28528
28723
  const nextjsMissingMetadata = defineRule({
28529
28724
  id: "nextjs-missing-metadata",
28530
28725
  title: "Page missing metadata for search previews",
@@ -28544,7 +28739,6 @@ const nextjsMissingMetadata = defineRule({
28544
28739
  if (isNodeOfType(declaration, "FunctionDeclaration")) return declaration.id?.name === "generateMetadata";
28545
28740
  return (statement.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ExportSpecifier") && isNodeOfType(specifier.exported, "Identifier") && METADATA_EXPORT_NAMES.includes(specifier.exported.name));
28546
28741
  })) return;
28547
- if (isRedirectOnlyDefaultExport(programNode, context)) return;
28548
28742
  if (hasAncestorMetadataLayout(context.filename ?? "")) return;
28549
28743
  context.report({
28550
28744
  node: programNode,
@@ -36803,6 +36997,19 @@ const noDefaultProps = defineRule({
36803
36997
  } })
36804
36998
  });
36805
36999
  //#endregion
37000
+ //#region src/plugin/utils/is-component-function.ts
37001
+ const isFunctionAssignedToComponent = (functionNode) => {
37002
+ let cursor = functionNode.parent ?? null;
37003
+ while (isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
37004
+ if (isNodeOfType(cursor, "VariableDeclarator") && isNodeOfType(cursor.id, "Identifier") && isUppercaseName(cursor.id.name)) return true;
37005
+ return isNodeOfType(cursor, "ExportDefaultDeclaration");
37006
+ };
37007
+ const isComponentFunction$1 = (node) => {
37008
+ if (!isFunctionLike$1(node)) return false;
37009
+ if (isNodeOfType(node, "FunctionDeclaration")) return !node.id || node.id.name === "default" || isUppercaseName(node.id.name) || isNodeOfType(node.parent, "ExportDefaultDeclaration");
37010
+ return isFunctionAssignedToComponent(node);
37011
+ };
37012
+ //#endregion
36806
37013
  //#region src/plugin/utils/create-component-prop-stack-tracker.ts
36807
37014
  const extractDestructuredPropNames = (params) => {
36808
37015
  const propNames = /* @__PURE__ */ new Set();
@@ -47186,103 +47393,6 @@ const EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1 = new Set([
47186
47393
  "useVisibility",
47187
47394
  "useWindowSize"
47188
47395
  ]);
47189
- const EXTERNAL_SUBSCRIPTION_PRIMITIVE_RESULT_HOOK_NAMES = new Set([
47190
- "useMatchMedia",
47191
- "useMediaQuery",
47192
- "useVisibility"
47193
- ]);
47194
- const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
47195
- const getImportedExternalSubscriptionHookName = (analysis, rawCallee) => {
47196
- const callee = stripParenExpression(rawCallee);
47197
- if (isNodeOfType(callee, "Identifier")) {
47198
- const calleeRef = getRef(analysis, callee);
47199
- if (!calleeRef || !isImportBindingRef(calleeRef)) return null;
47200
- const importBinding = getImportBindingForName(callee, callee.name);
47201
- if (!importBinding || importBinding.isNamespace) return null;
47202
- if (importBinding.exportedName && EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(importBinding.exportedName)) return importBinding.exportedName;
47203
- return importBinding.exportedName === "default" && EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(callee.name) ? callee.name : null;
47204
- }
47205
- if (!isNodeOfType(callee, "MemberExpression")) return null;
47206
- const hookName = getStaticMemberPropertyName(callee);
47207
- const namespaceIdentifier = stripParenExpression(callee.object);
47208
- if (!hookName || !EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(hookName) || !isNodeOfType(namespaceIdentifier, "Identifier")) return null;
47209
- const namespaceRef = getRef(analysis, namespaceIdentifier);
47210
- if (!namespaceRef || !isImportBindingRef(namespaceRef)) return null;
47211
- return getImportBindingForName(namespaceIdentifier, namespaceIdentifier.name)?.isNamespace ? hookName : null;
47212
- };
47213
- const isSafeExternalSubscriptionResultBinding = (bindingIdentifier, bindingPattern) => {
47214
- let current = bindingIdentifier;
47215
- let didCrossDestructuringBoundary = false;
47216
- while (current !== bindingPattern) {
47217
- const parent = current.parent;
47218
- if (!parent || isNodeOfType(parent, "AssignmentPattern") || isNodeOfType(parent, "RestElement")) return false;
47219
- if (isNodeOfType(parent, "Property")) {
47220
- if (parent.value !== current) return false;
47221
- didCrossDestructuringBoundary = true;
47222
- }
47223
- if (isNodeOfType(parent, "ArrayPattern")) didCrossDestructuringBoundary = true;
47224
- current = parent;
47225
- }
47226
- return didCrossDestructuringBoundary;
47227
- };
47228
- const getVariablesDefinedByDeclarator = (analysis, declarator) => analysis.scopeManager.scopes.flatMap((scope) => scope.variables).filter((variable) => variable.defs.some((definition) => definition.node === declarator));
47229
- const hasUnsafeExternalSubscriptionBindingUse = (analysis, variable, visitedVariables = /* @__PURE__ */ new Set()) => {
47230
- if (visitedVariables.has(variable)) return false;
47231
- visitedVariables.add(variable);
47232
- return variable.references.some((candidateReference) => {
47233
- if (candidateReference.init) return false;
47234
- if (candidateReference.isWrite()) return true;
47235
- let usageRoot = findTransparentExpressionRoot(candidateReference.identifier);
47236
- while (isNodeOfType(usageRoot.parent, "MemberExpression") && usageRoot.parent.object === usageRoot) usageRoot = findTransparentExpressionRoot(usageRoot.parent);
47237
- const usageParent = usageRoot.parent;
47238
- if (isNodeOfType(usageParent, "AssignmentExpression") && usageParent.left === usageRoot || isNodeOfType(usageParent, "UpdateExpression") && usageParent.argument === usageRoot || isNodeOfType(usageParent, "UnaryExpression") && usageParent.operator === "delete" && usageParent.argument === usageRoot) return true;
47239
- let usageAncestor = usageRoot.parent;
47240
- while (usageAncestor && !isNodeOfType(usageAncestor, "VariableDeclarator") && !isFunctionLike$1(usageAncestor) && !isNodeOfType(usageAncestor, "Program")) usageAncestor = usageAncestor.parent;
47241
- if (isNodeOfType(usageAncestor, "VariableDeclarator") && usageAncestor.init) {
47242
- const aliasVariables = getVariablesDefinedByDeclarator(analysis, usageAncestor);
47243
- return aliasVariables.length === 0 || aliasVariables.some((aliasVariable) => hasUnsafeExternalSubscriptionBindingUse(analysis, aliasVariable, visitedVariables));
47244
- }
47245
- return false;
47246
- });
47247
- };
47248
- const hasOnlySafeExternalSubscriptionResultBindings = (analysis, declarator, variables = getVariablesDefinedByDeclarator(analysis, declarator)) => {
47249
- return variables.length > 0 && variables.every((variable) => {
47250
- const bindingIdentifier = variable.defs.find((candidateDefinition) => candidateDefinition.node === declarator)?.name;
47251
- return Boolean(bindingIdentifier && isNodeOfType(bindingIdentifier, "Identifier") && isSafeExternalSubscriptionResultBinding(bindingIdentifier, declarator.id) && !hasUnsafeExternalSubscriptionBindingUse(analysis, variable));
47252
- });
47253
- };
47254
- const hasImmutableExternalSubscriptionCallResult = (analysis, rawCallee, relatedRefs, allowWholeResult) => {
47255
- const callee = stripParenExpression(rawCallee);
47256
- const callExpression = callee.parent;
47257
- if (!callExpression || !isNodeOfType(callExpression, "CallExpression") || callExpression.callee !== callee) return false;
47258
- const initializer = findTransparentExpressionRoot(callExpression);
47259
- const declarator = initializer.parent;
47260
- if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== initializer || !isNodeOfType(declarator.parent, "VariableDeclaration") || declarator.parent.kind !== "const") return false;
47261
- const relatedVariables = getVariablesDefinedByDeclarator(analysis, declarator).filter((variable) => relatedRefs.some((relatedRef) => relatedRef.resolved === variable));
47262
- if (relatedVariables.length === 0) return false;
47263
- return isNodeOfType(declarator.id, "Identifier") ? allowWholeResult && relatedVariables.every((variable) => !hasUnsafeExternalSubscriptionBindingUse(analysis, variable)) : hasOnlySafeExternalSubscriptionResultBindings(analysis, declarator, relatedVariables);
47264
- };
47265
- const isImmutableImportedExternalSubscriptionHookCallee = (analysis, rawCallee, relatedRefs) => {
47266
- const hookName = getImportedExternalSubscriptionHookName(analysis, rawCallee);
47267
- return Boolean(hookName && hasImmutableExternalSubscriptionCallResult(analysis, rawCallee, relatedRefs, EXTERNAL_SUBSCRIPTION_PRIMITIVE_RESULT_HOOK_NAMES.has(hookName)));
47268
- };
47269
- const isExternalSubscriptionHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
47270
- const declarator = def.node;
47271
- if (!isNodeOfType(declarator, "VariableDeclarator") || !declarator.init || !isNodeOfType(declarator.parent, "VariableDeclaration") || declarator.parent.kind !== "const") return false;
47272
- const initializer = stripParenExpression(declarator.init);
47273
- if (!isNodeOfType(initializer, "CallExpression")) return false;
47274
- const hookName = getImportedExternalSubscriptionHookName(analysis, initializer.callee);
47275
- if (!hookName) return false;
47276
- if (isNodeOfType(declarator.id, "Identifier")) return EXTERNAL_SUBSCRIPTION_PRIMITIVE_RESULT_HOOK_NAMES.has(hookName) && ref.resolved && !hasUnsafeExternalSubscriptionBindingUse(analysis, ref.resolved);
47277
- const bindingIdentifier = def.name;
47278
- return isNodeOfType(bindingIdentifier, "Identifier") && isSafeExternalSubscriptionResultBinding(bindingIdentifier, declarator.id) && ref.resolved && !hasUnsafeExternalSubscriptionBindingUse(analysis, ref.resolved);
47279
- }));
47280
- const isExternalSubscriptionHookResultArgument = (analysis, argument) => {
47281
- const unwrappedArgument = stripParenExpression(argument);
47282
- if (!isNodeOfType(unwrappedArgument, "Identifier")) return false;
47283
- const argumentRef = getRef(analysis, unwrappedArgument);
47284
- return Boolean(argumentRef && isExternalSubscriptionHookResultRef(analysis, argumentRef));
47285
- };
47286
47396
  const isCallbackPropReference = (analysis, ref) => {
47287
47397
  if (!isProp(analysis, ref)) return false;
47288
47398
  const identifier = ref.identifier;
@@ -47315,9 +47425,8 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
47315
47425
  if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
47316
47426
  return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
47317
47427
  };
47318
- const getLocalHookExternalStateProof = (analysis, ref, scopes) => {
47428
+ const getLocalHookExternalStateProof = (analysis, ref) => {
47319
47429
  let hookFunction = resolveToFunction(ref);
47320
- let didResolveThroughResultBinding = false;
47321
47430
  if (!hookFunction) for (const definition of ref.resolved?.defs ?? []) {
47322
47431
  const definitionNode = definition.node;
47323
47432
  if (!isNodeOfType(definitionNode, "VariableDeclarator") || !definitionNode.init) continue;
@@ -47328,27 +47437,29 @@ const getLocalHookExternalStateProof = (analysis, ref, scopes) => {
47328
47437
  const calleeReference = getRef(analysis, callee);
47329
47438
  if (!calleeReference) continue;
47330
47439
  hookFunction = resolveToFunction(calleeReference);
47331
- if (hookFunction) {
47332
- didResolveThroughResultBinding = true;
47333
- break;
47334
- }
47440
+ if (hookFunction) break;
47335
47441
  }
47336
47442
  if (!hookFunction) return null;
47337
- if (didResolveThroughResultBinding && (!ref.resolved || hasUnsafeExternalSubscriptionBindingUse(analysis, ref.resolved))) return false;
47338
- if (isNodeOfType(hookFunction, "ArrowFunctionExpression") && !isNodeOfType(hookFunction.body, "BlockStatement") && isReactHookCall(stripParenExpression(hookFunction.body), "useSyncExternalStore", scopes)) return true;
47339
- const returnStatements = collectFunctionReturnStatements(hookFunction);
47340
- if (returnStatements.length > 0 && returnStatements.every((returnStatement) => returnStatement.argument && isReactHookCall(stripParenExpression(returnStatement.argument), "useSyncExternalStore", scopes))) return true;
47341
- const returnedReferences = returnStatements.flatMap((returnStatement) => returnStatement.argument ? getDownstreamRefs(analysis, returnStatement.argument) : []);
47443
+ const returnedReferences = collectFunctionReturnStatements(hookFunction).flatMap((returnStatement) => returnStatement.argument ? getDownstreamRefs(analysis, returnStatement.argument) : []);
47342
47444
  if (returnedReferences.length === 0) return null;
47343
47445
  return returnedReferences.every((returnedReference) => isState(analysis, returnedReference) && isExternallyDrivenState(analysis, returnedReference));
47344
47446
  };
47345
- const isImmutableLocalExternalStoreHookCallee = (analysis, rawCallee, relatedRefs, scopes) => {
47346
- const callee = stripParenExpression(rawCallee);
47347
- if (!isNodeOfType(callee, "Identifier")) return false;
47348
- const calleeReference = getRef(analysis, callee);
47349
- if (!calleeReference || getLocalHookExternalStateProof(analysis, calleeReference, scopes) !== true) return false;
47350
- return hasImmutableExternalSubscriptionCallResult(analysis, callee, relatedRefs, true);
47447
+ const isExternalSubscriptionHookRef = (analysis, ref) => {
47448
+ const identifier = ref.identifier;
47449
+ if (!isNodeOfType(identifier, "Identifier")) return false;
47450
+ const localHookProof = getLocalHookExternalStateProof(analysis, ref);
47451
+ if (localHookProof !== null) return localHookProof;
47452
+ if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(identifier.name) && isCalleePosition(identifier)) return true;
47453
+ return Boolean(ref.resolved?.defs.some((def) => {
47454
+ const node = def.node;
47455
+ if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
47456
+ const initializer = stripParenExpression(node.init);
47457
+ if (!isNodeOfType(initializer, "CallExpression")) return false;
47458
+ const callee = stripParenExpression(initializer.callee);
47459
+ return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(callee.name);
47460
+ }));
47351
47461
  };
47462
+ const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
47352
47463
  const isCalleePosition = (identifier) => {
47353
47464
  const parent = identifier.parent;
47354
47465
  return Boolean(parent && (isNodeOfType(parent, "CallExpression") || isNodeOfType(parent, "NewExpression")) && parent.callee === identifier);
@@ -47409,35 +47520,25 @@ const noPassDataToParent = defineRule({
47409
47520
  return getFunctionalUpdaterDataRefs(analysis, argument);
47410
47521
  }
47411
47522
  if (isHandlerBagArgument(analysis, argument)) return [];
47412
- if (isExternalSubscriptionHookResultArgument(analysis, argument)) return [];
47413
47523
  if (isParentWiredHookResultArgument(analysis, argument)) return [];
47414
47524
  if (isNodeOfType(argument, "Identifier")) {
47415
47525
  const argumentRef = getRef(analysis, argument);
47416
47526
  if (argumentRef && resolveToFunction(argumentRef)) return [];
47417
47527
  }
47418
47528
  return getDownstreamRefs(analysis, argument);
47419
- }).flatMap((argumentRef) => {
47420
- if (isExternallyDrivenState(analysis, argumentRef) || getLocalHookExternalStateProof(analysis, argumentRef, context.scopes) === true) return [];
47421
- const upstreamRefs = getUpstreamRefs(analysis, argumentRef);
47422
- return upstreamRefs.filter((upstreamRef) => {
47423
- if (!isLeafRef(upstreamRef)) return false;
47424
- return !isImmutableImportedExternalSubscriptionHookCallee(analysis, upstreamRef.identifier, upstreamRefs) && !isImmutableLocalExternalStoreHookCallee(analysis, upstreamRef.identifier, upstreamRefs, context.scopes);
47425
- });
47426
- });
47427
- if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) {
47428
- const wrapperUpstreamRefs = getArgsUpstreamRefs(analysis, ref);
47429
- argsUpstreamRefs.push(...wrapperUpstreamRefs.filter((upstreamRef) => isLeafRef(upstreamRef) && !isImmutableImportedExternalSubscriptionHookCallee(analysis, upstreamRef.identifier, wrapperUpstreamRefs) && !isImmutableLocalExternalStoreHookCallee(analysis, upstreamRef.identifier, wrapperUpstreamRefs, context.scopes)));
47430
- }
47529
+ }).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) || isExternalSubscriptionHookRef(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
47530
+ if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
47431
47531
  if (!argsUpstreamRefs.some((argRef) => {
47432
- const argIdentifier = argRef.identifier;
47433
- if (isUseStateIdentifier(argIdentifier)) return false;
47532
+ if (isUseStateIdentifier(argRef.identifier)) return false;
47533
+ if (isExternalSubscriptionHookRef(analysis, argRef)) return false;
47434
47534
  if (isProp(analysis, argRef)) return false;
47435
- if (isUseRefIdentifier(argIdentifier)) return false;
47535
+ if (isUseRefIdentifier(argRef.identifier)) return false;
47436
47536
  if (isRefCurrent(argRef)) return false;
47437
47537
  if (isConstant(argRef)) return false;
47438
47538
  if (isParentWiredHookResultRef(analysis, argRef)) return false;
47439
47539
  if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
47440
47540
  if (resolveToFunction(argRef)) return false;
47541
+ const argIdentifier = argRef.identifier;
47441
47542
  if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
47442
47543
  if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
47443
47544
  return true;
@@ -59754,7 +59855,7 @@ const renderingUsetransitionLoading = defineRule({
59754
59855
  const isRepositorySecretFilePath = (relativePath) => DOTENV_FILE_PATTERN.test(relativePath) || /(?:^|\/)\.npmrc$/.test(relativePath) || /(?:^|\/)[^/]*(?:credential|credentials|service-account|serviceAccount|firebase-admin|google-service-account|gcp-service-account)[^/]*\.(?:json|env|pem|key)$/i.test(relativePath);
59755
59856
  //#endregion
59756
59857
  //#region src/plugin/rules/security-scan/repository-secret-file.ts
59757
- const isRepositorySecretExamplePath = (relativePath) => /(?:^|\/)\.env(?:\.[^./]+)*\.(?:example|sample|template|dist|defaults?)$|(?:^|\/)[^/]*(?:example|sample|template)[^/]*\.(?:env|json|pem|key)$/i.test(relativePath);
59858
+ const isRepositorySecretExamplePath = (relativePath) => /(?:^|\/)\.env\.(?:example|sample|template|dist|defaults?)$|(?:^|\/)[^/]*(?:example|sample|template)[^/]*\.(?:env|json|pem|key)$/i.test(relativePath);
59758
59859
  const repositorySecretFile = defineRule({
59759
59860
  id: "repository-secret-file",
59760
59861
  title: "Secret file checked into repository",
@@ -73173,6 +73274,18 @@ const reactDoctorRules = [
73173
73274
  requires: [...new Set(["react", ...jsxNoScriptUrl.requires ?? []])]
73174
73275
  }
73175
73276
  },
73277
+ {
73278
+ key: "react-doctor/jsx-no-target-blank",
73279
+ id: "jsx-no-target-blank",
73280
+ source: "react-doctor",
73281
+ originallyExternal: true,
73282
+ rule: {
73283
+ ...jsxNoTargetBlank,
73284
+ framework: "global",
73285
+ category: "Security",
73286
+ requires: [...new Set(["react", ...jsxNoTargetBlank.requires ?? []])]
73287
+ }
73288
+ },
73176
73289
  {
73177
73290
  key: "react-doctor/jsx-no-undef",
73178
73291
  id: "jsx-no-undef",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.8.1-dev.b4556e3",
3
+ "version": "0.8.1-dev.b852961",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",