oxlint-plugin-react-doctor 0.4.2-dev.727894a → 0.4.2-dev.af98f83

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Aiden Bai
3
+ Copyright (c) 2026 Million Software, Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/dist/index.d.ts CHANGED
@@ -1620,25 +1620,6 @@ declare const REACT_DOCTOR_RULES: readonly [{
1620
1620
  readonly recommendation?: string;
1621
1621
  readonly create: (context: RuleContext) => RuleVisitors;
1622
1622
  };
1623
- }, {
1624
- readonly key: "react-doctor/jsx-no-target-blank";
1625
- readonly id: "jsx-no-target-blank";
1626
- readonly source: "react-doctor";
1627
- readonly originallyExternal: true;
1628
- readonly rule: {
1629
- readonly framework: "global";
1630
- readonly category: "Security";
1631
- readonly id: string;
1632
- readonly title?: string;
1633
- readonly severity: RuleSeverity;
1634
- readonly requires?: ReadonlyArray<string>;
1635
- readonly disabledBy?: ReadonlyArray<string>;
1636
- readonly tags?: ReadonlyArray<string>;
1637
- readonly defaultEnabled?: boolean;
1638
- readonly lifecycle?: "retired";
1639
- readonly recommendation?: string;
1640
- readonly create: (context: RuleContext) => RuleVisitors;
1641
- };
1642
1623
  }, {
1643
1624
  readonly key: "react-doctor/jsx-no-undef";
1644
1625
  readonly id: "jsx-no-undef";
@@ -8108,25 +8089,6 @@ declare const RULES: readonly [{
8108
8089
  readonly recommendation?: string;
8109
8090
  readonly create: (context: RuleContext) => RuleVisitors;
8110
8091
  };
8111
- }, {
8112
- readonly key: "react-doctor/jsx-no-target-blank";
8113
- readonly id: "jsx-no-target-blank";
8114
- readonly source: "react-doctor";
8115
- readonly originallyExternal: true;
8116
- readonly rule: {
8117
- readonly framework: "global";
8118
- readonly category: "Security";
8119
- readonly id: string;
8120
- readonly title?: string;
8121
- readonly severity: RuleSeverity;
8122
- readonly requires?: ReadonlyArray<string>;
8123
- readonly disabledBy?: ReadonlyArray<string>;
8124
- readonly tags?: ReadonlyArray<string>;
8125
- readonly defaultEnabled?: boolean;
8126
- readonly lifecycle?: "retired";
8127
- readonly recommendation?: string;
8128
- readonly create: (context: RuleContext) => RuleVisitors;
8129
- };
8130
8092
  }, {
8131
8093
  readonly key: "react-doctor/jsx-no-undef";
8132
8094
  readonly id: "jsx-no-undef";
package/dist/index.js CHANGED
@@ -887,7 +887,7 @@ const PREFER_ALT = "Screen readers skip a decorative image more reliably with `a
887
887
  const MESSAGE_OBJECT = "Blind users can't use this `<object>` because screen readers can't describe it, so add `alt`, `aria-label`, `aria-labelledby`, `title`, or inner fallback text.";
888
888
  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`.";
889
889
  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`.";
890
- const resolveSettings$53 = (settings) => {
890
+ const resolveSettings$52 = (settings) => {
891
891
  const reactDoctor = settings?.["react-doctor"];
892
892
  return typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.altText ?? {} : {};
893
893
  };
@@ -1004,7 +1004,7 @@ const altText = defineRule({
1004
1004
  category: "Accessibility",
1005
1005
  create: (context) => {
1006
1006
  if (isNextjsMetadataImageRouteFilename(normalizeFilename$1(context.filename ?? ""))) return {};
1007
- const settings = resolveSettings$53(context.settings);
1007
+ const settings = resolveSettings$52(context.settings);
1008
1008
  const checkImg = !settings.elements || settings.elements.includes("img");
1009
1009
  const checkObject = !settings.elements || settings.elements.includes("object");
1010
1010
  const checkArea = !settings.elements || settings.elements.includes("area");
@@ -1048,7 +1048,7 @@ const DEFAULT_AMBIGUOUS = [
1048
1048
  "a link",
1049
1049
  "learn more"
1050
1050
  ];
1051
- const resolveSettings$52 = (settings) => {
1051
+ const resolveSettings$51 = (settings) => {
1052
1052
  const reactDoctor = settings?.["react-doctor"];
1053
1053
  return { words: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorAmbiguousText ?? {} : {}).words ?? DEFAULT_AMBIGUOUS };
1054
1054
  };
@@ -1089,7 +1089,7 @@ const anchorAmbiguousText = defineRule({
1089
1089
  recommendation: "Name where a link goes. Avoid 'click here', 'learn more', and 'link'.",
1090
1090
  category: "Accessibility",
1091
1091
  create: (context) => {
1092
- const settings = resolveSettings$52(context.settings);
1092
+ const settings = resolveSettings$51(context.settings);
1093
1093
  const ambiguousSet = new Set(settings.words.map((word) => word.toLowerCase()));
1094
1094
  return { JSXElement(node) {
1095
1095
  if (getElementType(node.openingElement, context.settings) !== "a") return;
@@ -1139,7 +1139,7 @@ const getStaticTemplateLiteralValue = (templateLiteral) => {
1139
1139
  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).";
1140
1140
  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.";
1141
1141
  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.";
1142
- const resolveSettings$51 = (settings) => {
1142
+ const resolveSettings$50 = (settings) => {
1143
1143
  const reactDoctor = settings?.["react-doctor"];
1144
1144
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorIsValid ?? {} : {};
1145
1145
  const jsxA11y = settings?.["jsx-a11y"];
@@ -1178,7 +1178,7 @@ const anchorIsValid = defineRule({
1178
1178
  recommendation: "Give links a real destination. Use `<button>` for in-page actions.",
1179
1179
  category: "Accessibility",
1180
1180
  create: (context) => {
1181
- const settings = resolveSettings$51(context.settings);
1181
+ const settings = resolveSettings$50(context.settings);
1182
1182
  return { JSXOpeningElement(node) {
1183
1183
  if (getElementType(node, context.settings) !== "a") return;
1184
1184
  let hrefAttribute;
@@ -2213,7 +2213,7 @@ const PRESENTATION_ROLES$2 = new Set(["presentation", "none"]);
2213
2213
  //#endregion
2214
2214
  //#region src/plugin/rules/a11y/aria-role.ts
2215
2215
  const buildBaseMessage = (suffix) => `Screen reader users get no help from this \`role\` because it isn't a valid ARIA role, so use a real, non-abstract role.${suffix}`;
2216
- const resolveSettings$50 = (settings) => {
2216
+ const resolveSettings$49 = (settings) => {
2217
2217
  const reactDoctor = settings?.["react-doctor"];
2218
2218
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.ariaRole ?? {} : {};
2219
2219
  return {
@@ -2229,7 +2229,7 @@ const ariaRole = defineRule({
2229
2229
  recommendation: "Use a real, non-abstract ARIA role.",
2230
2230
  category: "Accessibility",
2231
2231
  create: (context) => {
2232
- const settings = resolveSettings$50(context.settings);
2232
+ const settings = resolveSettings$49(context.settings);
2233
2233
  return { JSXOpeningElement(node) {
2234
2234
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
2235
2235
  if (!roleAttribute) return;
@@ -3255,7 +3255,7 @@ const FORM_CONTROL_TAGS = new Set([
3255
3255
  "select",
3256
3256
  "form"
3257
3257
  ]);
3258
- const resolveSettings$49 = (settings) => {
3258
+ const resolveSettings$48 = (settings) => {
3259
3259
  const reactDoctor = settings?.["react-doctor"];
3260
3260
  return { inputComponents: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {}).inputComponents ?? [] };
3261
3261
  };
@@ -3267,7 +3267,7 @@ const autocompleteValid = defineRule({
3267
3267
  recommendation: "Use a valid autofill token in `autoComplete`.",
3268
3268
  category: "Accessibility",
3269
3269
  create: (context) => {
3270
- const settings = resolveSettings$49(context.settings);
3270
+ const settings = resolveSettings$48(context.settings);
3271
3271
  return { JSXOpeningElement: (node) => {
3272
3272
  const tag = getElementType(node, context.settings);
3273
3273
  if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.includes(tag)) return;
@@ -3320,7 +3320,7 @@ const isCreateElementCall = (node) => {
3320
3320
  //#region src/plugin/rules/react-builtins/button-has-type.ts
3321
3321
  const MISSING_MESSAGE$2 = "Your users can submit the form by accident because a `<button>` with no `type` defaults to submit.";
3322
3322
  const INVALID_MESSAGE = "This button's `type` is invalid.";
3323
- const resolveSettings$48 = (settings) => {
3323
+ const resolveSettings$47 = (settings) => {
3324
3324
  const reactDoctor = settings?.["react-doctor"];
3325
3325
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.buttonHasType ?? {} : {};
3326
3326
  return {
@@ -3362,7 +3362,7 @@ const buttonHasType = defineRule({
3362
3362
  severity: "warn",
3363
3363
  recommendation: "Always set a `type` on a `<button>`: `type=\"button\"`, `\"submit\"`, or `\"reset\"`.",
3364
3364
  create: (context) => {
3365
- const settings = resolveSettings$48(context.settings);
3365
+ const settings = resolveSettings$47(context.settings);
3366
3366
  const isTestlikeFile = isTestlikeFilename(context.filename);
3367
3367
  return {
3368
3368
  JSXOpeningElement(node) {
@@ -3431,7 +3431,7 @@ const buttonHasType = defineRule({
3431
3431
  //#region src/plugin/rules/react-builtins/checked-requires-onchange-or-readonly.ts
3432
3432
  const MISSING_MESSAGE$1 = "Your users can't toggle this input because `checked` has no `onChange`.";
3433
3433
  const EXCLUSIVE_MESSAGE = "This input behaves unpredictably with both `checked` & `defaultChecked` set.";
3434
- const resolveSettings$47 = (settings) => {
3434
+ const resolveSettings$46 = (settings) => {
3435
3435
  const reactDoctor = settings?.["react-doctor"];
3436
3436
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.checkedRequiresOnchangeOrReadonly ?? {} : {};
3437
3437
  return {
@@ -3484,7 +3484,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
3484
3484
  recommendation: "Add `onChange` (controlled) or `readOnly` (display-only), or use `defaultChecked` for an uncontrolled checkbox.",
3485
3485
  category: "Correctness",
3486
3486
  create: (context) => {
3487
- const settings = resolveSettings$47(context.settings);
3487
+ const settings = resolveSettings$46(context.settings);
3488
3488
  const reportFromPresence = (presence) => {
3489
3489
  if (presence.checkedNode && presence.defaultCheckedNode && !settings.ignoreExclusiveCheckedAttribute) context.report({
3490
3490
  node: presence.checkedNode,
@@ -3708,7 +3708,7 @@ const HTML_FOR_ATTRIBUTE = "htmlFor";
3708
3708
  const LABEL_ELEMENT = "label";
3709
3709
  const DEFAULT_DEPTH = 5;
3710
3710
  const MAX_DEPTH = 25;
3711
- const resolveSettings$46 = (settings) => {
3711
+ const resolveSettings$45 = (settings) => {
3712
3712
  const reactDoctor = settings?.["react-doctor"];
3713
3713
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.controlHasAssociatedLabel ?? {} : {};
3714
3714
  return {
@@ -3828,7 +3828,7 @@ const controlHasAssociatedLabel = defineRule({
3828
3828
  recommendation: "Give every interactive control a label screen readers can read.",
3829
3829
  category: "Accessibility",
3830
3830
  create: (context) => {
3831
- const settings = resolveSettings$46(context.settings);
3831
+ const settings = resolveSettings$45(context.settings);
3832
3832
  const isTestlikeFile = isTestlikeFilename(context.filename);
3833
3833
  return { JSXElement(node) {
3834
3834
  if (isTestlikeFile) return;
@@ -4199,7 +4199,7 @@ const DEFAULT_ADDITIONAL_HOCS = [
4199
4199
  "lazy",
4200
4200
  "withTracking"
4201
4201
  ];
4202
- const resolveSettings$45 = (settings) => {
4202
+ const resolveSettings$44 = (settings) => {
4203
4203
  const reactDoctor = settings?.["react-doctor"];
4204
4204
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.displayName ?? {} : {};
4205
4205
  const additionalHoCs = new Set(ruleSettings.additionalHoCs ?? DEFAULT_ADDITIONAL_HOCS);
@@ -4391,7 +4391,7 @@ const displayName = defineRule({
4391
4391
  recommendation: "Give each component a `displayName` so DevTools shows a clear name.",
4392
4392
  category: "Architecture",
4393
4393
  create: (context) => {
4394
- const settings = resolveSettings$45(context.settings);
4394
+ const settings = resolveSettings$44(context.settings);
4395
4395
  const ignoreNamed = settings.ignoreTranspilerName ? false : true;
4396
4396
  const reportAt = (node) => {
4397
4397
  context.report({
@@ -6263,7 +6263,7 @@ const compileGlob = (pattern) => {
6263
6263
  //#endregion
6264
6264
  //#region src/plugin/rules/react-builtins/forbid-component-props.ts
6265
6265
  const DEFAULT_FORBID_PROPS = ["className", "style"];
6266
- const resolveSettings$44 = (settings) => {
6266
+ const resolveSettings$43 = (settings) => {
6267
6267
  const reactDoctor = settings?.["react-doctor"];
6268
6268
  const forbid = (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidComponentProps ?? {} : {}).forbid;
6269
6269
  if (!forbid || forbid.length === 0) return { forbid: DEFAULT_FORBID_PROPS };
@@ -6317,7 +6317,7 @@ const forbidComponentProps = defineRule({
6317
6317
  recommendation: "List the props you want to block per component in the `forbidComponentProps.forbid` setting.",
6318
6318
  category: "Architecture",
6319
6319
  create: (context) => {
6320
- const entries = resolveSettings$44(context.settings).forbid.map(normalizeEntry);
6320
+ const entries = resolveSettings$43(context.settings).forbid.map(normalizeEntry);
6321
6321
  return { JSXOpeningElement(node) {
6322
6322
  if (entries.length === 0) return;
6323
6323
  if (!isSupportedJsxName(node.name)) return;
@@ -6346,7 +6346,7 @@ const forbidComponentProps = defineRule({
6346
6346
  //#endregion
6347
6347
  //#region src/plugin/rules/react-builtins/forbid-dom-props.ts
6348
6348
  const buildMessage$23 = (propName, customMessage) => customMessage ?? `Your project blocks the \`${propName}\` prop on plain HTML tags.`;
6349
- const resolveSettings$43 = (settings) => {
6349
+ const resolveSettings$42 = (settings) => {
6350
6350
  const reactDoctor = settings?.["react-doctor"];
6351
6351
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
6352
6352
  const map = /* @__PURE__ */ new Map();
@@ -6368,7 +6368,7 @@ const forbidDomProps = defineRule({
6368
6368
  recommendation: "List the HTML props you want to block in the `forbidDomProps.forbid` setting.",
6369
6369
  category: "Architecture",
6370
6370
  create: (context) => {
6371
- const forbidMap = resolveSettings$43(context.settings);
6371
+ const forbidMap = resolveSettings$42(context.settings);
6372
6372
  return { JSXOpeningElement(node) {
6373
6373
  if (forbidMap.size === 0) return;
6374
6374
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -6455,7 +6455,7 @@ const isReactFunctionCall = (node, expectedCall) => {
6455
6455
  //#endregion
6456
6456
  //#region src/plugin/rules/react-builtins/forbid-elements.ts
6457
6457
  const buildMessage$22 = (element, customHelp) => customHelp ? `Your project blocks \`<${element}>\` here. ${customHelp}` : `Your project blocks \`<${element}>\` here.`;
6458
- const resolveSettings$42 = (settings) => {
6458
+ const resolveSettings$41 = (settings) => {
6459
6459
  const reactDoctor = settings?.["react-doctor"];
6460
6460
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
6461
6461
  const map = /* @__PURE__ */ new Map();
@@ -6471,7 +6471,7 @@ const forbidElements = defineRule({
6471
6471
  recommendation: "List the element names you want to block in the `forbidElements.forbid` setting.",
6472
6472
  category: "Architecture",
6473
6473
  create: (context) => {
6474
- const forbidMap = resolveSettings$42(context.settings);
6474
+ const forbidMap = resolveSettings$41(context.settings);
6475
6475
  return {
6476
6476
  JSXOpeningElement(node) {
6477
6477
  if (forbidMap.size === 0) return;
@@ -6542,7 +6542,7 @@ const DEFAULT_HEADING_TAGS = [
6542
6542
  "h5",
6543
6543
  "h6"
6544
6544
  ];
6545
- const resolveSettings$41 = (settings) => {
6545
+ const resolveSettings$40 = (settings) => {
6546
6546
  const reactDoctor = settings?.["react-doctor"];
6547
6547
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
6548
6548
  return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
@@ -6555,7 +6555,7 @@ const headingHasContent = defineRule({
6555
6555
  recommendation: "Put readable text in every heading.",
6556
6556
  category: "Accessibility",
6557
6557
  create: (context) => {
6558
- const settings = resolveSettings$41(context.settings);
6558
+ const settings = resolveSettings$40(context.settings);
6559
6559
  return { JSXOpeningElement(node) {
6560
6560
  const elementType = getElementType(node, context.settings);
6561
6561
  if (!settings.headingTags.includes(elementType)) return;
@@ -6575,7 +6575,7 @@ const headingHasContent = defineRule({
6575
6575
  //#region src/plugin/rules/react-builtins/hook-use-state.ts
6576
6576
  const REQUIRE_DESTRUCTURE_MESSAGE = "This `useState` result is hard to follow.";
6577
6577
  const NAMING_CONVENTION_MESSAGE = "The setter here is hard to spot.";
6578
- const resolveSettings$40 = (settings) => {
6578
+ const resolveSettings$39 = (settings) => {
6579
6579
  const reactDoctor = settings?.["react-doctor"];
6580
6580
  return { allowDestructuredState: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.hookUseState ?? {} : {}).allowDestructuredState ?? false };
6581
6581
  };
@@ -6602,7 +6602,7 @@ const hookUseState = defineRule({
6602
6602
  recommendation: "Destructure useState as `const [thing, setThing] = useState(…)`.",
6603
6603
  category: "Architecture",
6604
6604
  create: (context) => {
6605
- const { allowDestructuredState } = resolveSettings$40(context.settings);
6605
+ const { allowDestructuredState } = resolveSettings$39(context.settings);
6606
6606
  return { CallExpression(node) {
6607
6607
  if (!isReactFunctionCall(node, "useState")) return;
6608
6608
  const parent = node.parent;
@@ -6705,7 +6705,7 @@ const hooksNoNanInDeps = defineRule({
6705
6705
  //#endregion
6706
6706
  //#region src/plugin/rules/a11y/html-has-lang.ts
6707
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
- const resolveSettings$39 = (settings) => {
6708
+ const resolveSettings$38 = (settings) => {
6709
6709
  const reactDoctor = settings?.["react-doctor"];
6710
6710
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
6711
6711
  };
@@ -6742,7 +6742,7 @@ const htmlHasLang = defineRule({
6742
6742
  recommendation: "Set `<html lang=\"…\">` so screen readers know the page language.",
6743
6743
  category: "Accessibility",
6744
6744
  create: (context) => {
6745
- const settings = resolveSettings$39(context.settings);
6745
+ const settings = resolveSettings$38(context.settings);
6746
6746
  const tagSet = new Set(settings.htmlTags);
6747
6747
  return { JSXOpeningElement(node) {
6748
6748
  const tag = getElementType(node, context.settings);
@@ -7138,7 +7138,7 @@ const DEFAULT_REDUNDANT_WORDS = [
7138
7138
  "photo",
7139
7139
  "picture"
7140
7140
  ];
7141
- const resolveSettings$38 = (settings) => {
7141
+ const resolveSettings$37 = (settings) => {
7142
7142
  const reactDoctor = settings?.["react-doctor"];
7143
7143
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.imgRedundantAlt ?? {} : {};
7144
7144
  return {
@@ -7185,7 +7185,7 @@ const imgRedundantAlt = defineRule({
7185
7185
  recommendation: "Do not put 'image' or 'photo' in alt text. Describe what is shown.",
7186
7186
  category: "Accessibility",
7187
7187
  create: (context) => {
7188
- const settings = resolveSettings$38(context.settings);
7188
+ const settings = resolveSettings$37(context.settings);
7189
7189
  return { JSXOpeningElement(node) {
7190
7190
  const tag = getElementType(node, context.settings);
7191
7191
  if (!settings.components.includes(tag)) return;
@@ -7275,7 +7275,7 @@ const DEFAULT_TABBABLE_ROLES = [
7275
7275
  "switch",
7276
7276
  "textbox"
7277
7277
  ];
7278
- const resolveSettings$37 = (settings) => {
7278
+ const resolveSettings$36 = (settings) => {
7279
7279
  const reactDoctor = settings?.["react-doctor"];
7280
7280
  return { tabbable: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.interactiveSupportsFocus ?? {} : {}).tabbable ?? DEFAULT_TABBABLE_ROLES };
7281
7281
  };
@@ -7287,7 +7287,7 @@ const interactiveSupportsFocus = defineRule({
7287
7287
  recommendation: "Add `tabIndex` to elements with interactive roles and handlers.",
7288
7288
  category: "Accessibility",
7289
7289
  create: (context) => {
7290
- const settings = resolveSettings$37(context.settings);
7290
+ const settings = resolveSettings$36(context.settings);
7291
7291
  const tabbableSet = new Set(settings.tabbable);
7292
7292
  return { JSXOpeningElement(node) {
7293
7293
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
@@ -8585,7 +8585,7 @@ const jsTosortedImmutable = defineRule({
8585
8585
  const NEVER_MESSAGE$2 = () => `This prop is written inconsistently.`;
8586
8586
  const ALWAYS_MESSAGE$2 = () => `This prop is written inconsistently.`;
8587
8587
  const FALSE_OMITTED_MESSAGE = (attributeName) => `\`${attributeName}={false}\` does nothing.`;
8588
- const resolveSettings$36 = (settings) => {
8588
+ const resolveSettings$35 = (settings) => {
8589
8589
  const reactDoctor = settings?.["react-doctor"];
8590
8590
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxBooleanValue ?? {} : {};
8591
8591
  return {
@@ -8603,7 +8603,7 @@ const jsxBooleanValue = defineRule({
8603
8603
  recommendation: "Pick a single boolean-attribute style across the codebase (default: omit `={true}`).",
8604
8604
  category: "Architecture",
8605
8605
  create: (context) => {
8606
- const settings = resolveSettings$36(context.settings);
8606
+ const settings = resolveSettings$35(context.settings);
8607
8607
  const alwaysSet = new Set(settings.always);
8608
8608
  const neverSet = new Set(settings.never);
8609
8609
  return { JSXAttribute(node) {
@@ -8657,7 +8657,7 @@ const jsxBooleanValue = defineRule({
8657
8657
  const UNNECESSARY_BRACES_MESSAGE = "These curly braces do nothing here.";
8658
8658
  const REQUIRED_BRACES_MESSAGE = "This value needs curly braces `{ }` to read as an expression.";
8659
8659
  const isAllowedMode = (value) => value === "always" || value === "never" || value === "ignore";
8660
- const resolveSettings$35 = (settings) => {
8660
+ const resolveSettings$34 = (settings) => {
8661
8661
  const reactDoctor = settings?.["react-doctor"];
8662
8662
  const ruleSettingsRaw = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxCurlyBracePresence : void 0;
8663
8663
  if (isAllowedMode(ruleSettingsRaw)) return {
@@ -8753,7 +8753,7 @@ const jsxCurlyBracePresence = defineRule({
8753
8753
  recommendation: "Pick a consistent quoting style for JSX literal values.",
8754
8754
  category: "Architecture",
8755
8755
  create: (context) => {
8756
- const settings = resolveSettings$35(context.settings);
8756
+ const settings = resolveSettings$34(context.settings);
8757
8757
  return {
8758
8758
  JSXAttribute(node) {
8759
8759
  const value = node.value;
@@ -8836,7 +8836,7 @@ const containsJsxElement = (root) => {
8836
8836
  //#region src/plugin/rules/react-builtins/jsx-filename-extension.ts
8837
8837
  const JSX_NOT_ALLOWED = (extension) => `This file has JSX but a \`${extension}\` name.`;
8838
8838
  const EXTENSION_ONLY_FOR_JSX = (extension) => `\`${extension}\` files are meant for JSX, but this one has none.`;
8839
- const resolveSettings$34 = (settings) => {
8839
+ const resolveSettings$33 = (settings) => {
8840
8840
  const reactDoctor = settings?.["react-doctor"];
8841
8841
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFilenameExtension ?? {} : {};
8842
8842
  return {
@@ -8858,7 +8858,7 @@ const jsxFilenameExtension = defineRule({
8858
8858
  recommendation: "Name files with JSX `.jsx` or `.tsx`, or whatever extension your project uses.",
8859
8859
  category: "Architecture",
8860
8860
  create: (context) => {
8861
- const settings = resolveSettings$34(context.settings);
8861
+ const settings = resolveSettings$33(context.settings);
8862
8862
  const allowedExtensions = normalizeExtensions(settings.extensions);
8863
8863
  const filename = normalizeFilename$1(context.filename ?? "fixture.tsx");
8864
8864
  const extensionOnly = path.extname(filename).slice(1);
@@ -8912,7 +8912,7 @@ const isJsxFragmentElement = (node) => {
8912
8912
  //#region src/plugin/rules/react-builtins/jsx-fragments.ts
8913
8913
  const SYNTAX_MESSAGE = "This fragment is written inconsistently.";
8914
8914
  const ELEMENT_MESSAGE = "This fragment is written inconsistently.";
8915
- const resolveSettings$33 = (settings) => {
8915
+ const resolveSettings$32 = (settings) => {
8916
8916
  const reactDoctor = settings?.["react-doctor"];
8917
8917
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFragments ?? {} : {}).mode ?? "syntax" };
8918
8918
  };
@@ -8924,7 +8924,7 @@ const jsxFragments = defineRule({
8924
8924
  recommendation: "Pick one fragment style across the codebase.",
8925
8925
  category: "Architecture",
8926
8926
  create: (context) => {
8927
- const { mode } = resolveSettings$33(context.settings);
8927
+ const { mode } = resolveSettings$32(context.settings);
8928
8928
  return {
8929
8929
  JSXElement(node) {
8930
8930
  if (mode !== "syntax") return;
@@ -8968,7 +8968,7 @@ const buildPropRegex = (handlerPropPrefix) => {
8968
8968
  const escaped = tokens.map((token) => token.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("|");
8969
8969
  return new RegExp(`^(${escaped})[A-Z].*$`);
8970
8970
  };
8971
- const resolveSettings$32 = (settings) => {
8971
+ const resolveSettings$31 = (settings) => {
8972
8972
  const reactDoctor = settings?.["react-doctor"];
8973
8973
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxHandlerNames ?? {} : {};
8974
8974
  let handlerPrefix = DEFAULT_HANDLER_PREFIX;
@@ -9044,7 +9044,7 @@ const jsxHandlerNames = defineRule({
9044
9044
  recommendation: "Use the `on…` prefix for event-handler props and `handle…` for handlers.",
9045
9045
  category: "Architecture",
9046
9046
  create: (context) => {
9047
- const settings = resolveSettings$32(context.settings);
9047
+ const settings = resolveSettings$31(context.settings);
9048
9048
  return { JSXAttribute(node) {
9049
9049
  if (settings.ignoreComponentNames.length > 0) {
9050
9050
  const opening = node.parent;
@@ -9129,7 +9129,7 @@ const MISSING_KEY_ARRAY = "Your users can see the wrong data when this array reo
9129
9129
  const MISSING_KEY_ITERATOR = "Your users can see the wrong data when this list reorders.";
9130
9130
  const KEY_BEFORE_SPREAD = "The `{...spread}` can overwrite this `key` & break React's tracking.";
9131
9131
  const DUPLICATE_KEY = (keyValue) => `Your users can see the wrong data because two elements share the key "${keyValue}".`;
9132
- const resolveSettings$31 = (settings) => {
9132
+ const resolveSettings$30 = (settings) => {
9133
9133
  const reactDoctor = settings?.["react-doctor"];
9134
9134
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxKey ?? {} : {};
9135
9135
  return {
@@ -9277,7 +9277,7 @@ const jsxKey = defineRule({
9277
9277
  severity: "error",
9278
9278
  recommendation: "Add a `key={...}` prop to each element produced inside `.map` / array literal.",
9279
9279
  create: (context) => {
9280
- const settings = resolveSettings$31(context.settings);
9280
+ const settings = resolveSettings$30(context.settings);
9281
9281
  return {
9282
9282
  JSXElement(node) {
9283
9283
  const openingElement = node.openingElement;
@@ -9514,7 +9514,7 @@ const findVariableInitializer = (referenceNode, bindingName) => {
9514
9514
  //#region src/plugin/rules/react-builtins/jsx-max-depth.ts
9515
9515
  const buildMessage$18 = (depth, max) => `This JSX is hard to read at ${depth} levels deep, past the limit of ${max}.`;
9516
9516
  const DEFAULT_MAX_DEPTH = 14;
9517
- const resolveSettings$30 = (settings) => {
9517
+ const resolveSettings$29 = (settings) => {
9518
9518
  const reactDoctor = settings?.["react-doctor"];
9519
9519
  return { max: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxMaxDepth ?? {} : {}).max ?? DEFAULT_MAX_DEPTH };
9520
9520
  };
@@ -9574,7 +9574,7 @@ const jsxMaxDepth = defineRule({
9574
9574
  recommendation: "Pull deeply nested JSX into smaller components so it's easier to read.",
9575
9575
  category: "Architecture",
9576
9576
  create: (context) => {
9577
- const { max } = resolveSettings$30(context.settings);
9577
+ const { max } = resolveSettings$29(context.settings);
9578
9578
  const checkNode = (node) => {
9579
9579
  if (!isLeafJsxNode(node)) return;
9580
9580
  const total = computeJsxAncestorDepth(node) + computeChildrenDepth(node.children ?? [], /* @__PURE__ */ new Set());
@@ -11269,7 +11269,7 @@ const jsxNoNewObjectAsProp = defineRule({
11269
11269
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
11270
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
- const resolveSettings$29 = (settings) => {
11272
+ const resolveSettings$28 = (settings) => {
11273
11273
  const reactDoctor = settings?.["react-doctor"];
11274
11274
  if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
11275
11275
  return reactDoctor.jsxNoScriptUrl ?? {};
@@ -11295,7 +11295,7 @@ const jsxNoScriptUrl = defineRule({
11295
11295
  recommendation: "Replace `javascript:` URLs with `onClick` or `onSubmit` handlers. React 19 blocks them anyway.",
11296
11296
  category: "Security",
11297
11297
  create: (context) => {
11298
- const options = resolveSettings$29(context.settings);
11298
+ const options = resolveSettings$28(context.settings);
11299
11299
  return { JSXOpeningElement(node) {
11300
11300
  const elementName = getElementName(node);
11301
11301
  if (!elementName) return;
@@ -11315,287 +11315,6 @@ const jsxNoScriptUrl = defineRule({
11315
11315
  }
11316
11316
  });
11317
11317
  //#endregion
11318
- //#region src/plugin/rules/react-builtins/jsx-no-target-blank.ts
11319
- const NOREFERRER_MESSAGE = "`target=\"_blank\"` without `rel=\"noreferrer\"` lets the linked page hijack your tab to a phishing site.";
11320
- const NOOPENER_MESSAGE = "`target=\"_blank\"` without `rel` lets the linked page hijack your tab to a phishing site.";
11321
- const SPREAD_MESSAGE = "A spread here can add `target=\"_blank\"`, letting the linked page hijack your tab to a phishing site.";
11322
- const resolveSettings$28 = (settings) => {
11323
- const reactDoctor = settings?.["react-doctor"];
11324
- const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxNoTargetBlank ?? {} : {};
11325
- const reactSettings = typeof settings?.react === "object" && settings.react !== null ? settings.react : {};
11326
- const linkComponents = /* @__PURE__ */ new Map();
11327
- for (const entry of reactSettings.linkComponents ?? []) if (typeof entry === "string") linkComponents.set(entry, ["href"]);
11328
- else if (typeof entry === "object" && entry !== null) {
11329
- const linkAttribute = entry.linkAttribute ?? "href";
11330
- linkComponents.set(entry.name, Array.isArray(linkAttribute) ? linkAttribute : [linkAttribute]);
11331
- }
11332
- const formComponents = /* @__PURE__ */ new Map();
11333
- for (const entry of reactSettings.formComponents ?? []) if (typeof entry === "string") formComponents.set(entry, ["action"]);
11334
- else if (typeof entry === "object" && entry !== null) {
11335
- const formAttribute = entry.formAttribute ?? "action";
11336
- formComponents.set(entry.name, Array.isArray(formAttribute) ? formAttribute : [formAttribute]);
11337
- }
11338
- return {
11339
- enforceDynamicLinks: ruleSettings.enforceDynamicLinks ?? "always",
11340
- warnOnSpreadAttributes: ruleSettings.warnOnSpreadAttributes ?? false,
11341
- allowReferrer: ruleSettings.allowReferrer ?? false,
11342
- links: ruleSettings.links ?? true,
11343
- forms: ruleSettings.forms ?? false,
11344
- linkComponents,
11345
- formComponents
11346
- };
11347
- };
11348
- const isExternalLink = (href) => href.includes("//");
11349
- const matchHrefExpression = (expression, state) => {
11350
- if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") {
11351
- if (isExternalLink(expression.value)) state.isExternal = true;
11352
- return;
11353
- }
11354
- if (isNodeOfType(expression, "Identifier")) {
11355
- state.isDynamic = true;
11356
- return;
11357
- }
11358
- if (isNodeOfType(expression, "ConditionalExpression")) {
11359
- matchHrefExpression(expression.consequent, state);
11360
- matchHrefExpression(expression.alternate, state);
11361
- }
11362
- };
11363
- const checkHref = (attributeValue, enforceDynamicLinks) => {
11364
- const state = {
11365
- isExternal: false,
11366
- isDynamic: false
11367
- };
11368
- if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") state.isExternal = isExternalLink(attributeValue.value);
11369
- else if (isNodeOfType(attributeValue, "JSXExpressionContainer")) matchHrefExpression(attributeValue.expression, state);
11370
- if (enforceDynamicLinks === "never") return !state.isExternal || state.isDynamic;
11371
- return !(state.isExternal || state.isDynamic);
11372
- };
11373
- const checkRelValue = (text, allowReferrer) => {
11374
- const tokens = text.split(/\s+/);
11375
- if (allowReferrer) return tokens.includes("noopener") || tokens.includes("noreferrer");
11376
- return tokens.some((token) => token.toLowerCase() === "noreferrer");
11377
- };
11378
- const matchRelExpression = (expression, allowReferrer) => {
11379
- const empty = {
11380
- combined: false,
11381
- testName: "",
11382
- consequent: false,
11383
- alternate: false
11384
- };
11385
- if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return {
11386
- combined: checkRelValue(expression.value, allowReferrer),
11387
- testName: "",
11388
- consequent: false,
11389
- alternate: false
11390
- };
11391
- if (isNodeOfType(expression, "ConditionalExpression")) {
11392
- const consequent = matchRelExpression(expression.consequent, allowReferrer);
11393
- const alternate = matchRelExpression(expression.alternate, allowReferrer);
11394
- const test = expression.test;
11395
- if (isNodeOfType(test, "Identifier")) return {
11396
- combined: consequent.combined && alternate.combined,
11397
- testName: test.name,
11398
- consequent: consequent.combined,
11399
- alternate: alternate.combined
11400
- };
11401
- return {
11402
- combined: consequent.combined && alternate.combined,
11403
- testName: "",
11404
- consequent: consequent.combined,
11405
- alternate: alternate.combined
11406
- };
11407
- }
11408
- return empty;
11409
- };
11410
- const checkRel = (attributeValue, allowReferrer) => {
11411
- const empty = {
11412
- combined: false,
11413
- testName: "",
11414
- consequent: false,
11415
- alternate: false
11416
- };
11417
- if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return {
11418
- combined: checkRelValue(attributeValue.value, allowReferrer),
11419
- testName: "",
11420
- consequent: false,
11421
- alternate: false
11422
- };
11423
- if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
11424
- const expression = attributeValue.expression;
11425
- if (expression.type === "JSXEmptyExpression") return empty;
11426
- return matchRelExpression(expression, allowReferrer);
11427
- }
11428
- return empty;
11429
- };
11430
- const matchTargetExpression = (expression) => {
11431
- const empty = {
11432
- combined: false,
11433
- testName: "",
11434
- consequent: false,
11435
- alternate: false
11436
- };
11437
- if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return {
11438
- combined: expression.value.toLowerCase() === "_blank",
11439
- testName: "",
11440
- consequent: false,
11441
- alternate: false
11442
- };
11443
- if (isNodeOfType(expression, "ConditionalExpression")) {
11444
- const consequent = matchTargetExpression(expression.consequent);
11445
- const alternate = matchTargetExpression(expression.alternate);
11446
- const test = expression.test;
11447
- const combined = consequent.combined || alternate.combined;
11448
- if (isNodeOfType(test, "Identifier")) return {
11449
- combined,
11450
- testName: test.name,
11451
- consequent: consequent.combined,
11452
- alternate: alternate.combined
11453
- };
11454
- return {
11455
- combined,
11456
- testName: "",
11457
- consequent: consequent.combined,
11458
- alternate: alternate.combined
11459
- };
11460
- }
11461
- return empty;
11462
- };
11463
- const checkTarget = (attributeValue) => {
11464
- if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return {
11465
- combined: attributeValue.value.toLowerCase() === "_blank",
11466
- testName: "",
11467
- consequent: false,
11468
- alternate: false
11469
- };
11470
- if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
11471
- const expression = attributeValue.expression;
11472
- if (expression.type === "JSXEmptyExpression") return {
11473
- combined: false,
11474
- testName: "",
11475
- consequent: false,
11476
- alternate: false
11477
- };
11478
- return matchTargetExpression(expression);
11479
- }
11480
- return {
11481
- combined: false,
11482
- testName: "",
11483
- consequent: false,
11484
- alternate: false
11485
- };
11486
- };
11487
- const getOpeningElementName = (node) => {
11488
- const name = node.name;
11489
- if (isNodeOfType(name, "JSXIdentifier")) return name.name;
11490
- return null;
11491
- };
11492
- const jsxNoTargetBlank = defineRule({
11493
- id: "jsx-no-target-blank",
11494
- title: "Unsafe target=_blank link",
11495
- severity: "warn",
11496
- recommendation: "Add `rel=\"noreferrer\"` (or `\"noopener\"`) when using `target=\"_blank\"`.",
11497
- category: "Security",
11498
- create: (context) => {
11499
- const settings = resolveSettings$28(context.settings);
11500
- const isLink = (tagName) => {
11501
- if (!settings.links) return false;
11502
- if (tagName === "a") return true;
11503
- return settings.linkComponents.has(tagName);
11504
- };
11505
- const isForm = (tagName) => {
11506
- if (!settings.forms) return false;
11507
- if (tagName === "form") return true;
11508
- return settings.formComponents.has(tagName);
11509
- };
11510
- return { JSXOpeningElement(node) {
11511
- const tagName = getOpeningElementName(node);
11512
- if (!tagName) return;
11513
- if (!isLink(tagName) && !isForm(tagName)) return;
11514
- const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
11515
- const formAttributeNames = settings.formComponents.get(tagName) ?? ["action"];
11516
- let targetTuple = {
11517
- combined: false,
11518
- testName: "",
11519
- consequent: false,
11520
- alternate: false
11521
- };
11522
- let relTuple = {
11523
- combined: false,
11524
- testName: "",
11525
- consequent: false,
11526
- alternate: false
11527
- };
11528
- let isHrefValid = true;
11529
- let hasHrefValue = false;
11530
- let warnSpread = false;
11531
- let targetReportNode = node.name;
11532
- let spreadReportNode = null;
11533
- for (const attribute of node.attributes) {
11534
- if (isNodeOfType(attribute, "JSXSpreadAttribute")) {
11535
- if (settings.warnOnSpreadAttributes) {
11536
- warnSpread = true;
11537
- spreadReportNode = attribute;
11538
- targetTuple = {
11539
- combined: false,
11540
- testName: "",
11541
- consequent: false,
11542
- alternate: false
11543
- };
11544
- relTuple = {
11545
- combined: false,
11546
- testName: "",
11547
- consequent: false,
11548
- alternate: false
11549
- };
11550
- isHrefValid = false;
11551
- hasHrefValue = true;
11552
- }
11553
- continue;
11554
- }
11555
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
11556
- const attributeName = attribute.name;
11557
- if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
11558
- const propName = attributeName.name;
11559
- const value = attribute.value;
11560
- if (propName === "target") {
11561
- if (value) {
11562
- targetTuple = checkTarget(value);
11563
- targetReportNode = value;
11564
- }
11565
- } else if (propName === "href" || propName === "action" || linkAttributeNames.includes(propName) || formAttributeNames.includes(propName)) {
11566
- if (value) {
11567
- hasHrefValue = true;
11568
- isHrefValid = checkHref(value, settings.enforceDynamicLinks);
11569
- }
11570
- } else if (propName === "rel" && value) relTuple = checkRel(value, settings.allowReferrer);
11571
- }
11572
- if (warnSpread) {
11573
- if (hasHrefValue && isHrefValid || relTuple.combined) return;
11574
- context.report({
11575
- node: spreadReportNode ?? node,
11576
- message: SPREAD_MESSAGE
11577
- });
11578
- return;
11579
- }
11580
- if (!isHrefValid) {
11581
- if (targetTuple.testName !== "" && targetTuple.testName === relTuple.testName) {
11582
- const consequentBad = targetTuple.consequent && !relTuple.consequent;
11583
- const alternateBad = targetTuple.alternate && !relTuple.alternate;
11584
- if (consequentBad || alternateBad) context.report({
11585
- node: targetReportNode,
11586
- message: settings.allowReferrer ? NOOPENER_MESSAGE : NOREFERRER_MESSAGE
11587
- });
11588
- return;
11589
- }
11590
- if (targetTuple.combined && !relTuple.combined) context.report({
11591
- node: targetReportNode,
11592
- message: settings.allowReferrer ? NOOPENER_MESSAGE : NOREFERRER_MESSAGE
11593
- });
11594
- }
11595
- } };
11596
- }
11597
- });
11598
- //#endregion
11599
11318
  //#region src/plugin/rules/react-builtins/jsx-no-undef.ts
11600
11319
  const buildMessage$17 = (name) => `\`${name}\` crashes at runtime because it isn't defined here.`;
11601
11320
  const KNOWN_GLOBALS = new Set([
@@ -35999,17 +35718,6 @@ const reactDoctorRules = [
35999
35718
  category: "Security"
36000
35719
  }
36001
35720
  },
36002
- {
36003
- key: "react-doctor/jsx-no-target-blank",
36004
- id: "jsx-no-target-blank",
36005
- source: "react-doctor",
36006
- originallyExternal: true,
36007
- rule: {
36008
- ...jsxNoTargetBlank,
36009
- framework: "global",
36010
- category: "Security"
36011
- }
36012
- },
36013
35721
  {
36014
35722
  key: "react-doctor/jsx-no-undef",
36015
35723
  id: "jsx-no-undef",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.4.2-dev.727894a",
3
+ "version": "0.4.2-dev.af98f83",
4
4
  "description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",