oxlint-plugin-react-doctor 0.6.2-dev.59e8178 → 0.6.2-dev.ac71a3b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +78 -151
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -378,18 +378,13 @@ const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
378
378
  const isConfigOrCiPath = (relativePath) => /(?:^|\/)(?:package\.json|Dockerfile|docker-compose\.ya?ml|\.github\/workflows\/[^/]+\.ya?ml|vercel\.json|next\.config\.[cm]?[jt]s|netlify\.toml)$/i.test(relativePath);
379
379
  //#endregion
380
380
  //#region src/plugin/rules/security-scan/utils/is-production-file-path.ts
381
- const classificationCacheByPattern = /* @__PURE__ */ new Map();
382
381
  const isProductionFilePath = (relativePath, sourceFilePattern) => {
383
- let classificationByPath = classificationCacheByPattern.get(sourceFilePattern);
384
- if (!classificationByPath) {
385
- classificationByPath = /* @__PURE__ */ new Map();
386
- classificationCacheByPattern.set(sourceFilePattern, classificationByPath);
387
- }
388
- const cached = classificationByPath.get(relativePath);
389
- if (cached !== void 0) return cached;
390
- const isProduction = sourceFilePattern.test(relativePath) && !TEST_CONTEXT_PATTERN.test(relativePath) && !BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(relativePath) && !GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath);
391
- classificationByPath.set(relativePath, isProduction);
392
- return isProduction;
382
+ if (!sourceFilePattern.test(relativePath)) return false;
383
+ if (TEST_CONTEXT_PATTERN.test(relativePath)) return false;
384
+ if (BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath)) return false;
385
+ if (DOCUMENTATION_CONTEXT_PATTERN.test(relativePath)) return false;
386
+ if (GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath)) return false;
387
+ return true;
393
388
  };
394
389
  //#endregion
395
390
  //#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
@@ -1682,7 +1677,7 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
1682
1677
  };
1683
1678
  //#endregion
1684
1679
  //#region src/plugin/utils/normalize-filename.ts
1685
- const normalizeFilename = (filename) => filename.includes("\\") ? filename.replaceAll("\\", "/") : filename;
1680
+ const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
1686
1681
  //#endregion
1687
1682
  //#region src/plugin/utils/is-generated-image-render-context.ts
1688
1683
  const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
@@ -4843,8 +4838,7 @@ const FORM_CONTROL_TAGS = new Set([
4843
4838
  ]);
4844
4839
  const resolveSettings$48 = (settings) => {
4845
4840
  const reactDoctor = settings?.["react-doctor"];
4846
- const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {};
4847
- return { inputComponents: new Set(ruleSettings.inputComponents ?? []) };
4841
+ return { inputComponents: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {}).inputComponents ?? [] };
4848
4842
  };
4849
4843
  const autocompleteValid = defineRule({
4850
4844
  id: "autocomplete-valid",
@@ -4857,7 +4851,7 @@ const autocompleteValid = defineRule({
4857
4851
  const settings = resolveSettings$48(context.settings);
4858
4852
  return { JSXOpeningElement: (node) => {
4859
4853
  const tag = getElementType(node, context.settings);
4860
- if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.has(tag)) return;
4854
+ if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.includes(tag)) return;
4861
4855
  const attribute = hasJsxPropIgnoreCase(node.attributes, "autoComplete");
4862
4856
  if (!attribute) return;
4863
4857
  const value = getJsxPropStringValue(attribute);
@@ -8637,7 +8631,7 @@ const DEFAULT_HEADING_TAGS = [
8637
8631
  const resolveSettings$40 = (settings) => {
8638
8632
  const reactDoctor = settings?.["react-doctor"];
8639
8633
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
8640
- return { headingTags: new Set([...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []]) };
8634
+ return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
8641
8635
  };
8642
8636
  const headingHasContent = defineRule({
8643
8637
  id: "heading-has-content",
@@ -8650,7 +8644,7 @@ const headingHasContent = defineRule({
8650
8644
  const settings = resolveSettings$40(context.settings);
8651
8645
  return { JSXOpeningElement(node) {
8652
8646
  const elementType = getElementType(node, context.settings);
8653
- if (!settings.headingTags.has(elementType)) return;
8647
+ if (!settings.headingTags.includes(elementType)) return;
8654
8648
  const parent = node.parent;
8655
8649
  if (parent && isNodeOfType(parent, "JSXElement")) {
8656
8650
  if (objectHasAccessibleChild(parent, context.settings)) return;
@@ -9556,7 +9550,6 @@ const KEYBOARD_EVENT_HANDLERS = [
9556
9550
  "onKeyUp"
9557
9551
  ];
9558
9552
  const ALL_EVENT_HANDLERS = [...MOUSE_EVENT_HANDLERS, ...KEYBOARD_EVENT_HANDLERS];
9559
- const ALL_EVENT_HANDLERS_LOWER = new Set(ALL_EVENT_HANDLERS.map((handlerName) => handlerName.toLowerCase()));
9560
9553
  //#endregion
9561
9554
  //#region src/plugin/utils/is-disabled-element.ts
9562
9555
  const isDisabledElement = (openingElement) => {
@@ -9616,16 +9609,7 @@ const interactiveSupportsFocus = defineRule({
9616
9609
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
9617
9610
  const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
9618
9611
  if (!role) return;
9619
- let hasInteractiveHandler = false;
9620
- for (const attribute of node.attributes) {
9621
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
9622
- const attributeName = getJsxAttributeName(attribute.name);
9623
- if (attributeName && ALL_EVENT_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
9624
- hasInteractiveHandler = true;
9625
- break;
9626
- }
9627
- }
9628
- if (!hasInteractiveHandler) return;
9612
+ if (!ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)))) return;
9629
9613
  const elementType = getElementType(node, context.settings);
9630
9614
  if (!HTML_TAGS.has(elementType)) return;
9631
9615
  if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
@@ -14474,14 +14458,14 @@ const keyLifecycleRisk = defineRule({
14474
14458
  //#region src/plugin/rules/a11y/label-has-associated-control.ts
14475
14459
  const MESSAGE_NO_LABEL = "Blind users can't identify this field because screen readers find no label text, so add visible text, `aria-label`, or `aria-labelledby`.";
14476
14460
  const MESSAGE_NO_CONTROL = "Screen reader users can't tell which input this label names because it's tied to none, so add `htmlFor` or wrap the input inside it.";
14477
- const DEFAULT_CONTROL_COMPONENTS = new Set([
14461
+ const DEFAULT_CONTROL_COMPONENTS = [
14478
14462
  "input",
14479
14463
  "meter",
14480
14464
  "output",
14481
14465
  "progress",
14482
14466
  "select",
14483
14467
  "textarea"
14484
- ]);
14468
+ ];
14485
14469
  const DEFAULT_LABEL_ATTRIBUTES = [
14486
14470
  "alt",
14487
14471
  "aria-label",
@@ -14493,8 +14477,8 @@ const resolveSettings$24 = (settings) => {
14493
14477
  const jsxA11y = settings?.["jsx-a11y"];
14494
14478
  const forAttributes = (typeof jsxA11y === "object" && jsxA11y !== null ? jsxA11y : {}).attributes?.for ?? ["htmlFor"];
14495
14479
  return {
14496
- labelComponents: new Set(["label", ...ruleSettings.labelComponents ?? []]),
14497
- labelAttributes: new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []]),
14480
+ labelComponents: ["label", ...ruleSettings.labelComponents ?? []].sort(),
14481
+ labelAttributes: [...new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []])].sort(),
14498
14482
  controlComponents: ruleSettings.controlComponents ?? [],
14499
14483
  assert: ruleSettings.assert ?? "either",
14500
14484
  depth: Math.min(ruleSettings.depth ?? 5, 25),
@@ -14502,7 +14486,7 @@ const resolveSettings$24 = (settings) => {
14502
14486
  };
14503
14487
  };
14504
14488
  const isControlComponent = (tagName, controlComponents) => {
14505
- if (DEFAULT_CONTROL_COMPONENTS.has(tagName)) return true;
14489
+ if (DEFAULT_CONTROL_COMPONENTS.includes(tagName)) return true;
14506
14490
  return controlComponents.some((pattern) => compileGlob(pattern).test(tagName));
14507
14491
  };
14508
14492
  const searchForNestedControl = (child, currentDepth, searchContext) => {
@@ -14528,7 +14512,7 @@ const searchForAccessibleLabel = (child, currentDepth, searchContext) => {
14528
14512
  const attributeName = attribute.name;
14529
14513
  if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
14530
14514
  const propName = getJsxAttributeName(attributeName);
14531
- if (!propName || !searchContext.labelAttributes.has(propName)) continue;
14515
+ if (!propName || !searchContext.labelAttributes.includes(propName)) continue;
14532
14516
  const attributeValue = attribute.value;
14533
14517
  if (!attributeValue) continue;
14534
14518
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
@@ -14550,7 +14534,7 @@ const hasAccessibleLabel = (element, searchContext) => {
14550
14534
  const attributeName = attribute.name;
14551
14535
  if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
14552
14536
  const propName = getJsxAttributeName(attributeName);
14553
- if (propName && searchContext.labelAttributes.has(propName)) return true;
14537
+ if (propName && searchContext.labelAttributes.includes(propName)) return true;
14554
14538
  }
14555
14539
  for (const child of element.children) if (searchForAccessibleLabel(child, 1, searchContext)) return true;
14556
14540
  return false;
@@ -14573,7 +14557,7 @@ const labelHasAssociatedControl = defineRule({
14573
14557
  if (isTestlikeFile) return;
14574
14558
  const opening = node.openingElement;
14575
14559
  const tagName = getElementType(opening, context.settings);
14576
- if (!settings.labelComponents.has(tagName)) return;
14560
+ if (!settings.labelComponents.includes(tagName)) return;
14577
14561
  const hasHtmlFor = settings.forAttributes.some((attributeName) => Boolean(hasJsxPropIgnoreCase(opening.attributes, attributeName)));
14578
14562
  const searchContext = {
14579
14563
  depth: settings.depth,
@@ -15162,9 +15146,9 @@ const resolveSettings$23 = (settings) => {
15162
15146
  const reactDoctor = settings?.["react-doctor"];
15163
15147
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.mediaHasCaption ?? {} : {};
15164
15148
  return {
15165
- audio: new Set([...DEFAULT_AUDIO, ...ruleSettings.audio ?? []]),
15166
- video: new Set([...DEFAULT_VIDEO, ...ruleSettings.video ?? []]),
15167
- track: new Set([...DEFAULT_TRACK, ...ruleSettings.track ?? []])
15149
+ audio: [...DEFAULT_AUDIO, ...ruleSettings.audio ?? []],
15150
+ video: [...DEFAULT_VIDEO, ...ruleSettings.video ?? []],
15151
+ track: [...DEFAULT_TRACK, ...ruleSettings.track ?? []]
15168
15152
  };
15169
15153
  };
15170
15154
  const evaluateMuted = (attribute) => {
@@ -15193,7 +15177,7 @@ const childMayRenderTrack = (child, trackTags, settings) => {
15193
15177
  let rendersCaptionTrack = false;
15194
15178
  walkAst(expression, (inner) => {
15195
15179
  if (rendersCaptionTrack) return false;
15196
- if (isNodeOfType(inner, "JSXElement") && trackTags.has(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
15180
+ if (isNodeOfType(inner, "JSXElement") && trackTags.includes(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
15197
15181
  rendersCaptionTrack = true;
15198
15182
  return false;
15199
15183
  }
@@ -15211,7 +15195,7 @@ const mediaHasCaption = defineRule({
15211
15195
  const settings = resolveSettings$23(context.settings);
15212
15196
  return { JSXOpeningElement(node) {
15213
15197
  const tag = getElementType(node, context.settings);
15214
- if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
15198
+ if (!(settings.audio.includes(tag) || settings.video.includes(tag))) return;
15215
15199
  if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
15216
15200
  const parent = node.parent;
15217
15201
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
@@ -15226,7 +15210,7 @@ const mediaHasCaption = defineRule({
15226
15210
  if (!isNodeOfType(child, "JSXElement")) return false;
15227
15211
  const opening = child.openingElement;
15228
15212
  const childTag = getElementType(opening, context.settings);
15229
- if (!settings.track.has(childTag)) return false;
15213
+ if (!settings.track.includes(childTag)) return false;
15230
15214
  const kindAttribute = hasJsxPropIgnoreCase(opening.attributes, "kind");
15231
15215
  if (!kindAttribute) return false;
15232
15216
  let kindValue = kindAttribute.value;
@@ -15537,30 +15521,16 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
15537
15521
  const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
15538
15522
  const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
15539
15523
  const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
15540
- const collectSourceTextExportNames = (sourceText) => {
15524
+ const doesSourceTextExportName = (sourceText, exportedName) => {
15541
15525
  const strippedSource = stripJsComments(sourceText);
15542
- const exportedNames = /* @__PURE__ */ new Set();
15543
- if (DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) exportedNames.add("default");
15544
- for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1]) exportedNames.add(match[1]);
15545
- for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) {
15546
- const specifiersText = match[1] ?? "";
15547
- for (const specifier of parseExportSpecifiers(specifiersText, false)) exportedNames.add(specifier.exportedName);
15548
- }
15549
- return exportedNames;
15526
+ if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
15527
+ for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
15528
+ for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
15529
+ return false;
15550
15530
  };
15551
- const exportNamesCache = /* @__PURE__ */ new Map();
15552
15531
  const doesModuleExportName = (filePath, exportedName) => {
15553
15532
  try {
15554
- const fileStat = fs.statSync(filePath);
15555
- const cached = exportNamesCache.get(filePath);
15556
- if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.exportedNames.has(exportedName);
15557
- const exportedNames = collectSourceTextExportNames(fs.readFileSync(filePath, "utf8"));
15558
- exportNamesCache.set(filePath, {
15559
- mtimeMs: fileStat.mtimeMs,
15560
- size: fileStat.size,
15561
- exportedNames
15562
- });
15563
- return exportedNames.has(exportedName);
15533
+ return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
15564
15534
  } catch {
15565
15535
  return false;
15566
15536
  }
@@ -17443,15 +17413,8 @@ const getProgramAnalysis = (anyNode) => {
17443
17413
  programToAnalysis.set(programNode, analysis);
17444
17414
  return analysis;
17445
17415
  };
17446
- const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
17447
17416
  const getScopeForNode = (node, manager) => {
17448
17417
  if (!node.range) return null;
17449
- let scopeByNode = scopeByNodeCache.get(manager);
17450
- if (!scopeByNode) {
17451
- scopeByNode = /* @__PURE__ */ new WeakMap();
17452
- scopeByNodeCache.set(manager, scopeByNode);
17453
- }
17454
- if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
17455
17418
  let bestScope = null;
17456
17419
  let bestSize = Infinity;
17457
17420
  for (const scope of manager.scopes) {
@@ -17464,7 +17427,6 @@ const getScopeForNode = (node, manager) => {
17464
17427
  bestScope = scope;
17465
17428
  }
17466
17429
  }
17467
- scopeByNode.set(node, bestScope);
17468
17430
  return bestScope;
17469
17431
  };
17470
17432
  //#endregion
@@ -17499,20 +17461,11 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
17499
17461
  } else if (isAstNode(child)) descend(child, visit, visited);
17500
17462
  }
17501
17463
  };
17502
- const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
17503
17464
  const getUpstreamRefs = (analysis, ref) => {
17504
- let upstreamByRef = upstreamRefsCache.get(analysis);
17505
- if (!upstreamByRef) {
17506
- upstreamByRef = /* @__PURE__ */ new WeakMap();
17507
- upstreamRefsCache.set(analysis, upstreamByRef);
17508
- }
17509
- const cached = upstreamByRef.get(ref);
17510
- if (cached) return cached;
17511
17465
  const refs = [];
17512
17466
  ascend(analysis, ref, (upRef) => {
17513
17467
  refs.push(upRef);
17514
17468
  });
17515
- upstreamByRef.set(ref, refs);
17516
17469
  return refs;
17517
17470
  };
17518
17471
  const findDownstreamNodes = (topNode, type) => {
@@ -17522,24 +17475,11 @@ const findDownstreamNodes = (topNode, type) => {
17522
17475
  });
17523
17476
  return nodes;
17524
17477
  };
17525
- const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
17526
17478
  const getRef = (analysis, identifier) => {
17527
- let refByIdentifier = refByIdentifierCache.get(analysis);
17528
- if (!refByIdentifier) {
17529
- refByIdentifier = /* @__PURE__ */ new WeakMap();
17530
- refByIdentifierCache.set(analysis, refByIdentifier);
17531
- }
17532
- if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
17533
- let resolvedReference = null;
17534
17479
  const scope = getScopeForNode(identifier, analysis.scopeManager);
17535
- if (scope) {
17536
- for (const reference of scope.references) if (reference.identifier === identifier) {
17537
- resolvedReference = reference;
17538
- break;
17539
- }
17540
- }
17541
- refByIdentifier.set(identifier, resolvedReference);
17542
- return resolvedReference;
17480
+ if (!scope) return null;
17481
+ for (const reference of scope.references) if (reference.identifier === identifier) return reference;
17482
+ return null;
17543
17483
  };
17544
17484
  const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
17545
17485
  const getDownstreamRefs = (analysis, node) => {
@@ -17614,6 +17554,22 @@ const isEventualCallTo = (analysis, ref, predicate) => {
17614
17554
  };
17615
17555
  //#endregion
17616
17556
  //#region src/plugin/rules/state-and-effects/utils/effect/react.ts
17557
+ const getOuterScopeContaining = (analysis, node) => {
17558
+ if (!node.range) return null;
17559
+ let best = null;
17560
+ let bestSize = Infinity;
17561
+ for (const scope of analysis.scopeManager.scopes) {
17562
+ const block = scope.block;
17563
+ if (!block?.range) continue;
17564
+ if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
17565
+ const size = block.range[1] - block.range[0];
17566
+ if (size <= bestSize) {
17567
+ bestSize = size;
17568
+ best = scope;
17569
+ }
17570
+ }
17571
+ return best;
17572
+ };
17617
17573
  const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
17618
17574
  const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
17619
17575
  const isReactFunctionalComponent = (node) => {
@@ -17644,7 +17600,7 @@ const isReactFunctionalHOC = (analysis, node) => {
17644
17600
  const isWrappedSeparately = () => {
17645
17601
  if (!isNodeOfType(node.id, "Identifier")) return false;
17646
17602
  const bindingName = node.id.name;
17647
- const containingScope = getScopeForNode(node, analysis.scopeManager);
17603
+ const containingScope = getOuterScopeContaining(analysis, node);
17648
17604
  if (!containingScope) return false;
17649
17605
  const variable = containingScope.variables.find((v) => v.name === bindingName);
17650
17606
  if (!variable) return false;
@@ -24641,14 +24597,14 @@ const collectRoleBranches = (expression, out) => {
24641
24597
  out.hasOpaqueBranch = true;
24642
24598
  };
24643
24599
  const buildMessage$12 = (tag) => `Keyboard & screen reader users can't trigger this \`<${tag}>\` because it isn't interactive, so use a button or link or add an interactive role.`;
24644
- const INTERACTIVE_HANDLERS_LOWER = new Set([
24600
+ const INTERACTIVE_HANDLERS = [
24645
24601
  "onClick",
24646
24602
  "onMouseDown",
24647
24603
  "onMouseUp",
24648
24604
  "onKeyDown",
24649
24605
  "onKeyPress",
24650
24606
  "onKeyUp"
24651
- ].map((handlerName) => handlerName.toLowerCase()));
24607
+ ];
24652
24608
  const noNoninteractiveElementInteractions = defineRule({
24653
24609
  id: "no-noninteractive-element-interactions",
24654
24610
  title: "Handler on non-interactive element",
@@ -24663,16 +24619,7 @@ const noNoninteractiveElementInteractions = defineRule({
24663
24619
  const tag = getElementType(node, context.settings);
24664
24620
  if (!NON_INTERACTIVE_ELEMENTS.has(tag)) return;
24665
24621
  if (tag === "label") return;
24666
- let hasHandler = false;
24667
- for (const attribute of node.attributes) {
24668
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
24669
- const attributeName = getJsxAttributeName(attribute.name);
24670
- if (attributeName && INTERACTIVE_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
24671
- hasHandler = true;
24672
- break;
24673
- }
24674
- }
24675
- if (!hasHandler) return;
24622
+ if (!INTERACTIVE_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
24676
24623
  if (isHiddenFromScreenReader(node, context.settings)) return;
24677
24624
  const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
24678
24625
  if (roleAttr) {
@@ -25985,22 +25932,16 @@ const ELEMENT_ROLE_PAIRS = [
25985
25932
  ["tr", "row"],
25986
25933
  ["ul", "list"]
25987
25934
  ];
25988
- const EMPTY_ROLE_LIST = [];
25989
- const buildLookup = (pairs, keyIndex) => {
25990
- const lookup = /* @__PURE__ */ new Map();
25991
- for (const pair of pairs) {
25992
- const key = pair[keyIndex];
25993
- const value = pair[keyIndex === 0 ? 1 : 0];
25994
- const values = lookup.get(key);
25995
- if (!values) lookup.set(key, [value]);
25996
- else if (!values.includes(value)) values.push(value);
25997
- }
25998
- return lookup;
25935
+ const getElementImplicitRoles = (tag) => {
25936
+ const out = [];
25937
+ for (const [element, role] of ELEMENT_ROLE_PAIRS) if (element === tag && !out.includes(role)) out.push(role);
25938
+ return out;
25939
+ };
25940
+ const getTagsForRole = (role) => {
25941
+ const out = [];
25942
+ for (const [element, r] of ELEMENT_ROLE_PAIRS) if (r === role && !out.includes(element)) out.push(element);
25943
+ return out;
25999
25944
  };
26000
- const IMPLICIT_ROLES_BY_TAG = buildLookup(ELEMENT_ROLE_PAIRS, 0);
26001
- const TAGS_BY_ROLE = buildLookup(ELEMENT_ROLE_PAIRS, 1);
26002
- const getElementImplicitRoles = (tag) => IMPLICIT_ROLES_BY_TAG.get(tag) ?? EMPTY_ROLE_LIST;
26003
- const getTagsForRole = (role) => TAGS_BY_ROLE.get(role) ?? EMPTY_ROLE_LIST;
26004
25945
  //#endregion
26005
25946
  //#region src/plugin/utils/get-implicit-role.ts
26006
25947
  const getImplicitRole = (node, elementType) => {
@@ -26500,7 +26441,6 @@ const TANSTACK_ROUTE_PROPERTY_ORDER = [
26500
26441
  "headers",
26501
26442
  "remountDeps"
26502
26443
  ];
26503
- const TANSTACK_ROUTE_PROPERTY_INDEX = new Map(TANSTACK_ROUTE_PROPERTY_ORDER.map((propertyName, orderIndex) => [propertyName, orderIndex]));
26504
26444
  const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
26505
26445
  "createFileRoute",
26506
26446
  "createRoute",
@@ -26516,7 +26456,6 @@ const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
26516
26456
  "server",
26517
26457
  "handler"
26518
26458
  ];
26519
- const TANSTACK_MIDDLEWARE_METHOD_INDEX = new Map(TANSTACK_MIDDLEWARE_METHOD_ORDER.map((methodName, orderIndex) => [methodName, orderIndex]));
26520
26459
  const TANSTACK_REDIRECT_FUNCTIONS = new Set(["redirect", "notFound"]);
26521
26460
  const TANSTACK_SERVER_FN_FILE_PATTERN = /\.functions(\.[jt]sx?)?$/;
26522
26461
  const TANSTACK_QUERY_HOOKS = new Set([
@@ -27221,21 +27160,14 @@ const noStaticElementInteractions = defineRule({
27221
27160
  category: "Accessibility",
27222
27161
  create: (context) => {
27223
27162
  const settings = resolveSettings$12(context.settings);
27224
- const handlersLower = new Set(settings.handlers.map((handlerName) => handlerName.toLowerCase()));
27225
27163
  const isTestlikeFile = isTestlikeFilename(context.filename);
27226
27164
  return { JSXOpeningElement(node) {
27227
27165
  if (isTestlikeFile) return;
27228
27166
  let hasNonBlockerHandler = false;
27229
27167
  let hasAnyHandler = false;
27230
- let seenHandlerNames = null;
27231
- for (const attribute of node.attributes) {
27232
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
27233
- const attributeName = getJsxAttributeName(attribute.name);
27234
- if (!attributeName) continue;
27235
- const handlerNameLower = attributeName.toLowerCase();
27236
- if (!handlersLower.has(handlerNameLower)) continue;
27237
- if (seenHandlerNames?.has(handlerNameLower)) continue;
27238
- (seenHandlerNames ??= /* @__PURE__ */ new Set()).add(handlerNameLower);
27168
+ for (const handler of settings.handlers) {
27169
+ const attribute = hasJsxPropIgnoreCase(node.attributes, handler);
27170
+ if (!attribute) continue;
27239
27171
  if (isNullValue(attribute)) continue;
27240
27172
  hasAnyHandler = true;
27241
27173
  if (!isPureEventBlockerHandler(attribute)) {
@@ -28416,13 +28348,13 @@ const DOM_ATTRIBUTES_TO_CAMEL = new Map([
28416
28348
  ["xml:lang", "xmlLang"],
28417
28349
  ["xml:space", "xmlSpace"]
28418
28350
  ]);
28419
- const DOM_PROPERTIES_IGNORE_CASE_BY_LOWER = new Map([
28351
+ const DOM_PROPERTIES_IGNORE_CASE = [
28420
28352
  "charset",
28421
28353
  "allowFullScreen",
28422
28354
  "webkitAllowFullScreen",
28423
28355
  "mozAllowFullScreen",
28424
28356
  "webkitDirectory"
28425
- ].map((name) => [name.toLowerCase(), name]));
28357
+ ];
28426
28358
  //#endregion
28427
28359
  //#region src/plugin/constants/dom-property-tags.ts
28428
28360
  const DOM_PROPERTY_TO_ALLOWED_TAGS = new Map([
@@ -28626,7 +28558,10 @@ const matchesHtmlTagConventions = (tagName) => {
28626
28558
  if (!(firstCharacter >= 97 && firstCharacter <= 122)) return false;
28627
28559
  return !tagName.includes("-");
28628
28560
  };
28629
- const normalizeAttributeCase = (name) => DOM_PROPERTIES_IGNORE_CASE_BY_LOWER.get(name.toLowerCase()) ?? name;
28561
+ const normalizeAttributeCase = (name) => {
28562
+ for (const ignoreCaseName of DOM_PROPERTIES_IGNORE_CASE) if (ignoreCaseName.toLowerCase() === name.toLowerCase()) return ignoreCaseName;
28563
+ return name;
28564
+ };
28630
28565
  const hasUppercaseChar = (input) => /[A-Z]/.test(input);
28631
28566
  const INVALID_PROP_ON_TAG = (propName, allowedTags) => `React ignores \`${propName}\` here because it only works on these tags: ${allowedTags}.`;
28632
28567
  const DATA_LOWERCASE_REQUIRED = () => `React drops this \`data-*\` prop because of its capital letters.`;
@@ -34823,10 +34758,9 @@ const resolveTextBoundaryName = (openingElement) => {
34823
34758
  if (isNodeOfType(openingElement.name, "JSXNamespacedName")) return openingElement.name.namespace.name;
34824
34759
  return resolveJsxElementName(openingElement);
34825
34760
  };
34826
- const TEXT_COMPONENT_KEYWORDS = [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS];
34827
34761
  const isTextHandlingComponent = (elementName) => {
34828
34762
  if (REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return true;
34829
- return TEXT_COMPONENT_KEYWORDS.some((keyword) => elementName.includes(keyword));
34763
+ return [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS].some((keyword) => elementName.includes(keyword));
34830
34764
  };
34831
34765
  const isTransparentTextWrapper = (elementName) => elementName !== null && REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS.has(elementName);
34832
34766
  const isInsideTextHandlingComponent = (node) => {
@@ -41330,10 +41264,10 @@ const tanstackStartRoutePropertyOrder = defineRule({
41330
41264
  const propertyName = getPropertyKeyName(property);
41331
41265
  if (propertyName !== null) orderedPropertyNames.push(propertyName);
41332
41266
  }
41333
- const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_INDEX.has(propertyName));
41267
+ const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_ORDER.includes(propertyName));
41334
41268
  let lastIndex = -1;
41335
41269
  for (const propertyName of sensitiveProperties) {
41336
- const currentIndex = TANSTACK_ROUTE_PROPERTY_INDEX.get(propertyName) ?? -1;
41270
+ const currentIndex = TANSTACK_ROUTE_PROPERTY_ORDER.indexOf(propertyName);
41337
41271
  if (currentIndex < lastIndex) {
41338
41272
  const expectedBefore = TANSTACK_ROUTE_PROPERTY_ORDER[lastIndex];
41339
41273
  context.report({
@@ -41370,10 +41304,10 @@ const tanstackStartServerFnMethodOrder = defineRule({
41370
41304
  } else return;
41371
41305
  const ownMethodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
41372
41306
  if (methodNames[methodNames.length - 1] !== ownMethodName) return;
41373
- const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_INDEX.has(toMethodOrderToken(name)));
41307
+ const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_ORDER.includes(toMethodOrderToken(name)));
41374
41308
  let lastIndex = -1;
41375
41309
  for (const methodName of orderSensitiveMethods) {
41376
- const currentIndex = TANSTACK_MIDDLEWARE_METHOD_INDEX.get(toMethodOrderToken(methodName)) ?? -1;
41310
+ const currentIndex = TANSTACK_MIDDLEWARE_METHOD_ORDER.indexOf(toMethodOrderToken(methodName));
41377
41311
  if (currentIndex < lastIndex) {
41378
41312
  const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
41379
41313
  context.report({
@@ -41664,14 +41598,7 @@ const getStaticPropertyName = (member) => {
41664
41598
  if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
41665
41599
  return null;
41666
41600
  };
41667
- const importInfoCache = /* @__PURE__ */ new WeakMap();
41668
41601
  const getImportInfoForIdentifier = (identifier) => {
41669
- if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
41670
- const importInfo = computeImportInfoForIdentifier(identifier);
41671
- importInfoCache.set(identifier, importInfo);
41672
- return importInfo;
41673
- };
41674
- const computeImportInfoForIdentifier = (identifier) => {
41675
41602
  const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
41676
41603
  if (!specifier) return null;
41677
41604
  const declaration = specifier.parent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.6.2-dev.59e8178",
3
+ "version": "0.6.2-dev.ac71a3b",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",