oxlint-plugin-react-doctor 0.6.2-dev.fa61c20 → 0.6.2-dev.fc75a3e

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 +132 -205
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1458,12 +1458,6 @@ const getElementType = (openingElement, settings) => {
1458
1458
  return elementType;
1459
1459
  };
1460
1460
  //#endregion
1461
- //#region src/plugin/utils/has-jsx-a11y-settings.ts
1462
- const hasJsxA11ySettings = (settings) => {
1463
- const block = settings?.["jsx-a11y"];
1464
- return typeof block === "object" && block !== null;
1465
- };
1466
- //#endregion
1467
1461
  //#region src/plugin/utils/find-import-source-for-name.ts
1468
1462
  const collectFromProgram = (programRoot) => {
1469
1463
  const lookup = /* @__PURE__ */ new Map();
@@ -2016,12 +2010,7 @@ const altText = defineRule({
2016
2010
  const objectAliases = new Set(settings.object ?? []);
2017
2011
  const areaAliases = new Set(settings.area ?? []);
2018
2012
  const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
2019
- const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
2020
2013
  return { JSXOpeningElement(node) {
2021
- if (!fileHasJsxA11ySettings && isNodeOfType(node.name, "JSXIdentifier")) {
2022
- const rawName = node.name.name;
2023
- if (rawName !== "img" && rawName !== "object" && rawName !== "area" && rawName.toLowerCase() !== "input" && !imgAliases.has(rawName) && !objectAliases.has(rawName) && !areaAliases.has(rawName) && !inputImageAliases.has(rawName)) return;
2024
- }
2025
2014
  if (isGeneratedImageRenderContext(context, node)) return;
2026
2015
  const tag = getElementType(node, context.settings);
2027
2016
  if (checkImg && (tag === "img" || imgAliases.has(tag))) {
@@ -2204,9 +2193,7 @@ const anchorIsValid = defineRule({
2204
2193
  category: "Accessibility",
2205
2194
  create: (context) => {
2206
2195
  const settings = resolveSettings$50(context.settings);
2207
- const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
2208
2196
  return { JSXOpeningElement(node) {
2209
- if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
2210
2197
  if (getElementType(node, context.settings) !== "a") return;
2211
2198
  let hrefAttribute;
2212
2199
  for (const attributeName of settings.hrefAttributeNames) {
@@ -4890,8 +4877,9 @@ const isCreateElementCall = (node) => {
4890
4877
  const callee = node.callee;
4891
4878
  if (isNodeOfType(callee, "Identifier")) return callee.name === "createElement";
4892
4879
  if (isNodeOfType(callee, "MemberExpression")) {
4893
- if (!(callee.computed ? isNodeOfType(callee.property, "Literal") && callee.property.value === "createElement" : isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement")) return false;
4894
- return !memberChainContainsDocument(callee.object);
4880
+ if (memberChainContainsDocument(callee.object)) return false;
4881
+ if (callee.computed) return isNodeOfType(callee.property, "Literal") && callee.property.value === "createElement";
4882
+ return isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement";
4895
4883
  }
4896
4884
  return false;
4897
4885
  };
@@ -5958,7 +5946,6 @@ const noRedundantPaddingAxes = defineRule({
5958
5946
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5959
5947
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5960
5948
  if (!classNameLiteral) return;
5961
- if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
5962
5949
  if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
5963
5950
  const matchedPairs = collectAxisShorthandPairs(classNameLiteral, PADDING_HORIZONTAL_AXIS_PATTERN, PADDING_VERTICAL_AXIS_PATTERN);
5964
5951
  if (matchedPairs.length === 0) return;
@@ -5983,7 +5970,6 @@ const noRedundantSizeAxes = defineRule({
5983
5970
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5984
5971
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5985
5972
  if (!classNameLiteral) return;
5986
- if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
5987
5973
  if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
5988
5974
  const matchedPairs = collectAxisShorthandPairs(classNameLiteral, SIZE_WIDTH_AXIS_PATTERN, SIZE_HEIGHT_AXIS_PATTERN);
5989
5975
  if (matchedPairs.length === 0) return;
@@ -6008,7 +5994,6 @@ const noSpaceOnFlexChildren = defineRule({
6008
5994
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
6009
5995
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
6010
5996
  if (!classNameLiteral) return;
6011
- if (!classNameLiteral.includes("space-")) return;
6012
5997
  const tokens = tokenizeClassName(classNameLiteral);
6013
5998
  let hasFlexOrGridLayout = false;
6014
5999
  for (const token of tokens) {
@@ -9281,10 +9266,10 @@ const imgRedundantAlt = defineRule({
9281
9266
  if (isGeneratedImageRenderContext(context)) return {};
9282
9267
  const settings = resolveSettings$37(context.settings);
9283
9268
  return { JSXOpeningElement(node) {
9269
+ if (isGeneratedImageRenderContext(context, node)) return;
9284
9270
  const tag = getElementType(node, context.settings);
9285
9271
  if (!settings.components.includes(tag)) return;
9286
9272
  if (isHiddenFromScreenReader(node, context.settings)) return;
9287
- if (isGeneratedImageRenderContext(context, node)) return;
9288
9273
  const altAttribute = hasJsxPropIgnoreCase(node.attributes, "alt");
9289
9274
  if (!altAttribute) return;
9290
9275
  if (altValueRedundant(altAttribute, settings.words)) context.report({
@@ -9339,7 +9324,6 @@ const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|c
9339
9324
  const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
9340
9325
  const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
9341
9326
  const SECURITY_CONTEXT_WINDOW_CHARS = 250;
9342
- const CRYPTO_SURFACE_TRIGGER_PATTERN = /createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i;
9343
9327
  const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
9344
9328
  for (const callMatch of content.matchAll(pattern)) {
9345
9329
  const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
@@ -9369,7 +9353,6 @@ const insecureCryptoRisk = defineRule({
9369
9353
  if (!isProductionSourcePath(file.relativePath)) return [];
9370
9354
  if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
9371
9355
  if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
9372
- if (!CRYPTO_SURFACE_TRIGGER_PATTERN.test(file.content)) return [];
9373
9356
  const content = getScannableContent(file);
9374
9357
  let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
9375
9358
  if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
@@ -9602,16 +9585,15 @@ const interactiveSupportsFocus = defineRule({
9602
9585
  const settings = resolveSettings$36(context.settings);
9603
9586
  const tabbableSet = new Set(settings.tabbable);
9604
9587
  return { JSXOpeningElement(node) {
9605
- if (node.attributes.length === 0) return;
9606
9588
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
9607
9589
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
9608
9590
  const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
9609
- if (!role) return;
9610
- if (!ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)))) return;
9611
9591
  const elementType = getElementType(node, context.settings);
9612
9592
  if (!HTML_TAGS.has(elementType)) return;
9613
- if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
9593
+ const hasInteractiveHandler = ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)));
9614
9594
  const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
9595
+ if (!hasInteractiveHandler || isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
9596
+ if (!role) return;
9615
9597
  if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
9616
9598
  const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
9617
9599
  context.report({
@@ -10711,6 +10693,7 @@ const jsHoistIntl = defineRule({
10711
10693
  recommendation: "Move `new Intl.NumberFormat(...)` to the top of the file or wrap it in `useMemo`. Building one is slow, so don't redo it on every call",
10712
10694
  create: (context) => ({ NewExpression(node) {
10713
10695
  if (!isIntlNewExpression(node)) return;
10696
+ if (isInsideCacheMemo(node)) return;
10714
10697
  let cursor = node.parent ?? null;
10715
10698
  let inFunctionBody = false;
10716
10699
  while (cursor) {
@@ -10727,7 +10710,6 @@ const jsHoistIntl = defineRule({
10727
10710
  cursor = cursor.parent ?? null;
10728
10711
  }
10729
10712
  if (!inFunctionBody) return;
10730
- if (isInsideCacheMemo(node)) return;
10731
10713
  const className = isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : "Intl";
10732
10714
  context.report({
10733
10715
  node,
@@ -15669,12 +15651,8 @@ const containsFetchCall = (node, options) => {
15669
15651
  const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
15670
15652
  let didFindFetchCall = false;
15671
15653
  walkAst(node, (child) => {
15672
- if (didFindFetchCall) return false;
15673
15654
  if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
15674
- if (isFetchCall$1(child)) {
15675
- didFindFetchCall = true;
15676
- return false;
15677
- }
15655
+ if (isFetchCall$1(child)) didFindFetchCall = true;
15678
15656
  });
15679
15657
  return didFindFetchCall;
15680
15658
  };
@@ -16364,18 +16342,16 @@ const nextjsNoSideEffectInGetHandler = defineRule({
16364
16342
  recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
16365
16343
  create: (context) => {
16366
16344
  let resolveBinding = () => null;
16367
- let isRouteHandlerFile = false;
16368
- let mutatingSegment = null;
16369
16345
  return {
16370
16346
  Program(node) {
16371
16347
  resolveBinding = buildProgramBindingLookup(node);
16372
- const filename = normalizeFilename(context.filename ?? "");
16373
- isRouteHandlerFile = ROUTE_HANDLER_FILE_PATTERN.test(filename) && !CRON_ROUTE_PATTERN.test(filename);
16374
- mutatingSegment = isRouteHandlerFile ? extractMutatingRouteSegment(filename) : null;
16375
16348
  },
16376
16349
  ExportNamedDeclaration(node) {
16377
- if (!isRouteHandlerFile) return;
16350
+ const filename = normalizeFilename(context.filename ?? "");
16351
+ if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
16352
+ if (CRON_ROUTE_PATTERN.test(filename)) return;
16378
16353
  if (!isExportedGetHandler(node)) return;
16354
+ const mutatingSegment = extractMutatingRouteSegment(filename);
16379
16355
  const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
16380
16356
  for (const handlerBody of handlerBodies) {
16381
16357
  const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
@@ -19459,8 +19435,8 @@ const CONTEXT_MODULES = [
19459
19435
  ];
19460
19436
  const isCreateContextCallee = (callee) => {
19461
19437
  if (isNodeOfType(callee, "Identifier")) {
19462
- const binding = getImportBindingForName(callee, callee.name);
19463
- return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
19438
+ for (const moduleName of CONTEXT_MODULES) if (getImportedNameFromModule(callee, callee.name, moduleName) === "createContext") return true;
19439
+ return false;
19464
19440
  }
19465
19441
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
19466
19442
  const namespaceIdentifier = callee.object;
@@ -19470,8 +19446,8 @@ const isCreateContextCallee = (callee) => {
19470
19446
  if (propertyIdentifier.name !== "createContext") return false;
19471
19447
  const namespaceName = namespaceIdentifier.name;
19472
19448
  if (isCanonicalReactNamespaceName(namespaceName)) return true;
19473
- const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
19474
- return importSource !== null && CONTEXT_MODULES.includes(importSource);
19449
+ for (const moduleName of CONTEXT_MODULES) if (isImportedFromModule(namespaceIdentifier, namespaceName, moduleName)) return true;
19450
+ return false;
19475
19451
  }
19476
19452
  return false;
19477
19453
  };
@@ -26167,14 +26143,14 @@ const noRenderInRender = defineRule({
26167
26143
  const expression = node.expression;
26168
26144
  if (!isNodeOfType(expression, "CallExpression")) return;
26169
26145
  let calleeName = null;
26170
- if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
26171
- else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
26172
- if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
26173
26146
  if (isNodeOfType(expression.callee, "Identifier")) {
26174
26147
  if (tracesToPropOrParameter(context.scopes.symbolFor(expression.callee), context.scopes)) return;
26175
- } else if (isNodeOfType(expression.callee, "MemberExpression")) {
26148
+ calleeName = expression.callee.name;
26149
+ } else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) {
26176
26150
  if (rootsInProps(expression.callee.object, context.scopes)) return;
26151
+ calleeName = expression.callee.property.name;
26177
26152
  }
26153
+ if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
26178
26154
  context.report({
26179
26155
  node: expression,
26180
26156
  message: `Your users lose state because "${calleeName}()" builds UI from an inline call that React remounts, so pull it into its own component instead.`
@@ -28889,26 +28865,32 @@ const isFirstArgumentOfHocCall = (node) => {
28889
28865
  if (!isHocCallee$1(parent)) return false;
28890
28866
  return parent.arguments[0] === node;
28891
28867
  };
28892
- const MAP_LIKE_METHOD_NAMES = new Set([
28893
- "map",
28894
- "forEach",
28895
- "filter",
28896
- "flatMap",
28897
- "reduce",
28898
- "reduceRight"
28899
- ]);
28900
28868
  const isReturnOfMapCallback = (node) => {
28901
28869
  const parent = node.parent;
28902
28870
  if (!parent) return false;
28903
28871
  if (isNodeOfType(parent, "CallExpression")) {
28904
28872
  const callee = parent.callee;
28905
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
28873
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return [
28874
+ "map",
28875
+ "forEach",
28876
+ "filter",
28877
+ "flatMap",
28878
+ "reduce",
28879
+ "reduceRight"
28880
+ ].includes(callee.property.name);
28906
28881
  }
28907
28882
  if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
28908
28883
  const callbackParent = parent.parent;
28909
28884
  if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
28910
28885
  const callee = callbackParent.callee;
28911
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
28886
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return [
28887
+ "map",
28888
+ "forEach",
28889
+ "filter",
28890
+ "flatMap",
28891
+ "reduce",
28892
+ "reduceRight"
28893
+ ].includes(callee.property.name);
28912
28894
  }
28913
28895
  }
28914
28896
  return false;
@@ -28944,10 +28926,10 @@ const isRenderFlowingReadReference = (identifier) => {
28944
28926
  valueNode = parent;
28945
28927
  parent = parent.parent;
28946
28928
  continue;
28947
- case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isReactComponentName(parent.id.name);
28929
+ case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isUppercaseName(parent.id.name);
28948
28930
  case "AssignmentExpression": {
28949
28931
  const assignmentTarget = parent.left;
28950
- return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isReactComponentName(assignmentTarget.name);
28932
+ return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isUppercaseName(assignmentTarget.name);
28951
28933
  }
28952
28934
  case "Property":
28953
28935
  if (parent.value !== valueNode) return false;
@@ -29040,14 +29022,14 @@ const noUnstableNestedComponents = defineRule({
29040
29022
  };
29041
29023
  return {
29042
29024
  JSXOpeningElement(node) {
29043
- if (isNodeOfType(node.name, "JSXIdentifier") && isReactComponentName(node.name.name)) {
29025
+ if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) {
29044
29026
  recordInstantiation(node.name, node.name.name);
29045
29027
  return;
29046
29028
  }
29047
29029
  if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
29048
29030
  },
29049
29031
  Identifier(node) {
29050
- if (!isReactComponentName(node.name)) return;
29032
+ if (!isUppercaseName(node.name)) return;
29051
29033
  if (!isRenderFlowingReadReference(node)) return;
29052
29034
  recordInstantiation(node, node.name);
29053
29035
  },
@@ -30857,13 +30839,13 @@ const preferStableEmptyFallback = defineRule({
30857
30839
  memoRegistry = buildSameFileMemoRegistry(node);
30858
30840
  },
30859
30841
  JSXAttribute(node) {
30842
+ if (!isInsideFunctionScope(node)) return;
30843
+ if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
30860
30844
  if (!node.value) return;
30861
30845
  if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
30862
30846
  const innerExpression = node.value.expression;
30863
30847
  if (!innerExpression) return;
30864
30848
  if (innerExpression.type === "JSXEmptyExpression") return;
30865
- if (!isInsideFunctionScope(node)) return;
30866
- if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
30867
30849
  const parentJsxOpening = node.parent;
30868
30850
  const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
30869
30851
  if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
@@ -33456,7 +33438,6 @@ const RECYCLABLE_LIST_PACKAGES = {
33456
33438
  FlashList: ["@shopify/flash-list"],
33457
33439
  LegendList: ["@legendapp/list"]
33458
33440
  };
33459
- const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
33460
33441
  const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
33461
33442
  const RENDER_ITEM_PROP_NAMES = new Set([
33462
33443
  "renderItem",
@@ -33699,42 +33680,33 @@ const rnListMissingEstimatedItemSize = defineRule({
33699
33680
  requires: ["react-native"],
33700
33681
  severity: "warn",
33701
33682
  recommendation: "Without `estimatedItemSize` the list guesses row height and can flash blank cells on fast scroll. Add `estimatedItemSize={<avg-row-height-in-px>}` so it matches your rows.",
33702
- create: (context) => {
33703
- let fileImportsRecycler = false;
33704
- return {
33705
- Program(node) {
33706
- fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
33707
- },
33708
- JSXOpeningElement(node) {
33709
- if (!fileImportsRecycler) return;
33710
- const localElementName = resolveJsxElementName(node);
33711
- if (!localElementName) return;
33712
- const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
33713
- if (canonicalRecyclerName === null) return;
33714
- if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
33715
- let hasSizingHint = false;
33716
- let dataIsEmptyLiteral = false;
33717
- let hasDataProp = false;
33718
- for (const attribute of node.attributes ?? []) {
33719
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
33720
- if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
33721
- const attributeName = attribute.name.name;
33722
- if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
33723
- if (attributeName === "data") {
33724
- hasDataProp = true;
33725
- if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
33726
- }
33727
- }
33728
- if (hasSizingHint) return;
33729
- if (dataIsEmptyLiteral) return;
33730
- if (!hasDataProp) return;
33731
- context.report({
33732
- node,
33733
- message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
33734
- });
33683
+ create: (context) => ({ JSXOpeningElement(node) {
33684
+ const localElementName = resolveJsxElementName(node);
33685
+ if (!localElementName) return;
33686
+ const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
33687
+ if (canonicalRecyclerName === null) return;
33688
+ if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
33689
+ let hasSizingHint = false;
33690
+ let dataIsEmptyLiteral = false;
33691
+ let hasDataProp = false;
33692
+ for (const attribute of node.attributes ?? []) {
33693
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
33694
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
33695
+ const attributeName = attribute.name.name;
33696
+ if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
33697
+ if (attributeName === "data") {
33698
+ hasDataProp = true;
33699
+ if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
33735
33700
  }
33736
- };
33737
- }
33701
+ }
33702
+ if (hasSizingHint) return;
33703
+ if (dataIsEmptyLiteral) return;
33704
+ if (!hasDataProp) return;
33705
+ context.report({
33706
+ node,
33707
+ message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
33708
+ });
33709
+ } })
33738
33710
  });
33739
33711
  //#endregion
33740
33712
  //#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
@@ -33745,34 +33717,25 @@ const rnListRecyclableWithoutTypes = defineRule({
33745
33717
  requires: ["react-native"],
33746
33718
  severity: "warn",
33747
33719
  recommendation: "When rows have different shapes, reused cells can show the wrong layout. Add `getItemType={item => item.kind}` so FlashList keeps a separate pool per row type.",
33748
- create: (context) => {
33749
- let fileImportsRecycler = false;
33750
- return {
33751
- Program(node) {
33752
- fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
33753
- },
33754
- JSXOpeningElement(node) {
33755
- if (!fileImportsRecycler) return;
33756
- const elementName = resolveJsxElementName(node);
33757
- if (!elementName) return;
33758
- if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
33759
- let hasRecycleItemsEnabled = false;
33760
- let hasGetItemType = false;
33761
- for (const attr of node.attributes ?? []) {
33762
- if (!isNodeOfType(attr, "JSXAttribute")) continue;
33763
- if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
33764
- if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
33765
- else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
33766
- else hasRecycleItemsEnabled = true;
33767
- if (attr.name.name === "getItemType") hasGetItemType = true;
33768
- }
33769
- if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
33770
- node,
33771
- message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
33772
- });
33773
- }
33774
- };
33775
- }
33720
+ create: (context) => ({ JSXOpeningElement(node) {
33721
+ const elementName = resolveJsxElementName(node);
33722
+ if (!elementName) return;
33723
+ if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
33724
+ let hasRecycleItemsEnabled = false;
33725
+ let hasGetItemType = false;
33726
+ for (const attr of node.attributes ?? []) {
33727
+ if (!isNodeOfType(attr, "JSXAttribute")) continue;
33728
+ if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
33729
+ if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
33730
+ else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
33731
+ else hasRecycleItemsEnabled = true;
33732
+ if (attr.name.name === "getItemType") hasGetItemType = true;
33733
+ }
33734
+ if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
33735
+ node,
33736
+ message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
33737
+ });
33738
+ } })
33776
33739
  });
33777
33740
  //#endregion
33778
33741
  //#region src/plugin/rules/react-native/rn-no-deep-imports.ts
@@ -34802,7 +34765,6 @@ const rnNoRawText = defineRule({
34802
34765
  return {
34803
34766
  Program(programNode) {
34804
34767
  isDomComponentFile = hasDirective(programNode, "use dom");
34805
- if (!containsJsxElement(programNode)) return;
34806
34768
  const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
34807
34769
  autoDetectedTextWrappers = childrenForwarding.textWrappers;
34808
34770
  autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
@@ -38757,7 +38719,14 @@ const roleSupportsAriaProps = defineRule({
38757
38719
  recommendation: "Only use `aria-*` attributes that the element's role supports.",
38758
38720
  category: "Accessibility",
38759
38721
  create: (context) => ({ JSXOpeningElement(node) {
38760
- let ariaAttributes = null;
38722
+ const elementType = getElementType(node, context.settings);
38723
+ const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
38724
+ const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
38725
+ if (!role) return;
38726
+ if (!VALID_ARIA_ROLES.has(role)) return;
38727
+ const isImplicit = !roleAttribute;
38728
+ const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
38729
+ if (!supported) return;
38761
38730
  for (const attribute of node.attributes) {
38762
38731
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
38763
38732
  const attributeNode = attribute;
@@ -38767,21 +38736,6 @@ const roleSupportsAriaProps = defineRule({
38767
38736
  const propName = propRawName.toLowerCase();
38768
38737
  if (!propName.startsWith("aria-")) continue;
38769
38738
  if (!ARIA_PROPERTIES.has(propName)) continue;
38770
- (ariaAttributes ??= []).push({
38771
- attribute,
38772
- propName
38773
- });
38774
- }
38775
- if (!ariaAttributes) return;
38776
- const elementType = getElementType(node, context.settings);
38777
- const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
38778
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
38779
- if (!role) return;
38780
- if (!VALID_ARIA_ROLES.has(role)) return;
38781
- const isImplicit = !roleAttribute;
38782
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
38783
- if (!supported) return;
38784
- for (const { attribute, propName } of ariaAttributes) {
38785
38739
  if (supported.has(propName)) continue;
38786
38740
  context.report({
38787
38741
  node: attribute,
@@ -39619,7 +39573,6 @@ const serverCacheWithObjectLiteral = defineRule({
39619
39573
  cachedFunctionNames.add(node.id.name);
39620
39574
  },
39621
39575
  CallExpression(node) {
39622
- if (cachedFunctionNames.size === 0) return;
39623
39576
  if (!isNodeOfType(node.callee, "Identifier")) return;
39624
39577
  if (!cachedFunctionNames.has(node.callee.name)) return;
39625
39578
  const firstArg = node.arguments?.[0];
@@ -41578,7 +41531,6 @@ const webhookSignatureRisk = defineRule({
41578
41531
  //#endregion
41579
41532
  //#region src/plugin/rules/zod/utils/zod-ast.ts
41580
41533
  const ZOD_MODULE = "zod";
41581
- const ZOD_MODULE_SOURCES = [ZOD_MODULE];
41582
41534
  const getStaticPropertyName = (member) => {
41583
41535
  const property = member.property;
41584
41536
  if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
@@ -41722,33 +41674,25 @@ const zodV4NoDeprecatedErrorApis = defineRule({
41722
41674
  tags: ["migration-hint"],
41723
41675
  severity: "warn",
41724
41676
  recommendation: "Use the Zod 4 helpers instead: `z.treeifyError()`, `z.flattenError()`, `z.prettifyError()`, or read `error.issues` directly.",
41725
- create: (context) => {
41726
- let fileImportsZod = false;
41727
- return {
41728
- Program(node) {
41729
- fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
41730
- },
41731
- CallExpression(node) {
41732
- if (!fileImportsZod) return;
41733
- if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
41734
- if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
41735
- context.report({
41736
- node,
41737
- message: ZOD_ERROR_API_MESSAGE
41738
- });
41739
- },
41740
- MemberExpression(node) {
41741
- if (!fileImportsZod) return;
41742
- const parent = node.parent;
41743
- if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41744
- if (!isDeprecatedZodErrorMemberAccess(node)) return;
41745
- context.report({
41746
- node,
41747
- message: ZOD_ERROR_API_MESSAGE
41748
- });
41749
- }
41750
- };
41751
- }
41677
+ create: (context) => ({
41678
+ CallExpression(node) {
41679
+ if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
41680
+ if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
41681
+ context.report({
41682
+ node,
41683
+ message: ZOD_ERROR_API_MESSAGE
41684
+ });
41685
+ },
41686
+ MemberExpression(node) {
41687
+ const parent = node.parent;
41688
+ if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41689
+ if (!isDeprecatedZodErrorMemberAccess(node)) return;
41690
+ context.report({
41691
+ node,
41692
+ message: ZOD_ERROR_API_MESSAGE
41693
+ });
41694
+ }
41695
+ })
41752
41696
  });
41753
41697
  //#endregion
41754
41698
  //#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
@@ -41940,24 +41884,16 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
41940
41884
  tags: ["migration-hint"],
41941
41885
  severity: "warn",
41942
41886
  recommendation: "Switch to the Zod 4 versions: top-level factories like `z.enum()`, object helpers like `z.strictObject()`, the new `z.function({ input, output })` form, and explicit key/value schemas for `z.record()`.",
41943
- create: (context) => {
41944
- let fileImportsZod = false;
41945
- return {
41946
- Program(node) {
41947
- fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
41948
- },
41949
- CallExpression(node) {
41950
- if (!fileImportsZod) return;
41951
- if (isCallToDeprecatedTopLevelFactory(node) || isCallToDroppedCreateFactory(node) || isSingleArgumentRecordCall(node) || isLiteralSymbolCall(node) || isDeprecatedFunctionChainCall(node) || isDirectMethodCallOnZodFactory(node, OBJECT_FACTORY, OBJECT_METHODS) || isDirectMethodCallOnZodFactory(node, NUMBER_FACTORY, NUMBER_METHODS) || isRefineSecondArgumentFunction(node)) reportSchemaMigration(context, node);
41952
- },
41953
- MemberExpression(node) {
41954
- if (!fileImportsZod) return;
41955
- const parent = node.parent;
41956
- if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41957
- if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
41958
- }
41959
- };
41960
- }
41887
+ create: (context) => ({
41888
+ CallExpression(node) {
41889
+ if (isCallToDeprecatedTopLevelFactory(node) || isCallToDroppedCreateFactory(node) || isSingleArgumentRecordCall(node) || isLiteralSymbolCall(node) || isDeprecatedFunctionChainCall(node) || isDirectMethodCallOnZodFactory(node, OBJECT_FACTORY, OBJECT_METHODS) || isDirectMethodCallOnZodFactory(node, NUMBER_FACTORY, NUMBER_METHODS) || isRefineSecondArgumentFunction(node)) reportSchemaMigration(context, node);
41890
+ },
41891
+ MemberExpression(node) {
41892
+ const parent = node.parent;
41893
+ if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41894
+ if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
41895
+ }
41896
+ })
41961
41897
  });
41962
41898
  //#endregion
41963
41899
  //#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
@@ -41992,22 +41928,13 @@ const zodV4PreferTopLevelStringFormats = defineRule({
41992
41928
  tags: ["migration-hint"],
41993
41929
  severity: "warn",
41994
41930
  recommendation: "Use the Zod 4 top-level format checks like `z.email()`, `z.uuid()`, or `z.ipv4()` instead of `z.string().<format>()`.",
41995
- create: (context) => {
41996
- let fileImportsZod = false;
41997
- return {
41998
- Program(node) {
41999
- fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
42000
- },
42001
- CallExpression(node) {
42002
- if (!fileImportsZod) return;
42003
- if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
42004
- context.report({
42005
- node,
42006
- message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
42007
- });
42008
- }
42009
- };
42010
- }
41931
+ create: (context) => ({ CallExpression(node) {
41932
+ if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
41933
+ context.report({
41934
+ node,
41935
+ message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
41936
+ });
41937
+ } })
42011
41938
  });
42012
41939
  //#endregion
42013
41940
  //#region src/plugin/rule-registry.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.6.2-dev.fa61c20",
3
+ "version": "0.6.2-dev.fc75a3e",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",