oxlint-plugin-react-doctor 0.2.18-dev.8691177 → 0.2.18-dev.87440ab

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4609,11 +4609,17 @@ const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscr
4609
4609
  if (!isFunctionLike$2(node)) return false;
4610
4610
  return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
4611
4611
  };
4612
- const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
4612
+ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
4613
4613
  if (!returnedValue) return false;
4614
4614
  const unwrappedValue = unwrapChainExpression$2(returnedValue);
4615
- if (isNodeOfType(unwrappedValue, "Identifier")) return knownCleanupFunctionNames.has(unwrappedValue.name);
4615
+ if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
4616
+ if (isNodeOfType(unwrappedValue, "Identifier")) {
4617
+ if (unwrappedValue.name === "undefined") return false;
4618
+ if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
4619
+ return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
4620
+ }
4616
4621
  if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
4622
+ if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
4617
4623
  if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
4618
4624
  return false;
4619
4625
  };
@@ -4634,12 +4640,14 @@ const findSubscribeLikeUsages = (callback) => {
4634
4640
  if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
4635
4641
  usages.push({
4636
4642
  kind: "timer",
4643
+ node: child,
4637
4644
  resourceName: child.callee.name
4638
4645
  });
4639
4646
  return;
4640
4647
  }
4641
4648
  if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
4642
4649
  kind: "subscribe",
4650
+ node: child,
4643
4651
  resourceName: child.callee.property.name
4644
4652
  });
4645
4653
  });
@@ -4686,7 +4694,20 @@ const collectCleanupBindings = (effectCallback) => {
4686
4694
  });
4687
4695
  return bindings;
4688
4696
  };
4689
- const effectHasCleanupRelease = (callback) => {
4697
+ const getRangeStart = (node) => {
4698
+ const rangeStart = node.range?.[0];
4699
+ return typeof rangeStart === "number" ? rangeStart : null;
4700
+ };
4701
+ const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
4702
+ if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
4703
+ const returnStart = getRangeStart(returnStatement);
4704
+ if (returnStart === null) return true;
4705
+ return usages.some((usage) => {
4706
+ const usageStart = getRangeStart(usage.node);
4707
+ return usageStart === null || usageStart < returnStart;
4708
+ });
4709
+ };
4710
+ const effectHasCleanupReturn = (callback, usages) => {
4690
4711
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
4691
4712
  if (!isNodeOfType(callback.body, "BlockStatement")) return isCleanupReturningSubscribeLikeCallExpression(callback.body);
4692
4713
  const cleanupBindings = collectCleanupBindings(callback);
@@ -4694,7 +4715,8 @@ const effectHasCleanupRelease = (callback) => {
4694
4715
  walkInsideStatementBlocks(callback.body, (child) => {
4695
4716
  if (didFindCleanupReturn) return;
4696
4717
  if (!isNodeOfType(child, "ReturnStatement")) return;
4697
- if (isCleanupReturn(child.argument, cleanupBindings.cleanupFunctionNames, cleanupBindings.subscriptionNames)) didFindCleanupReturn = true;
4718
+ if (!cleanupReturnRunsAfterUsage(child, usages)) return;
4719
+ if (isCleanupReturn(child.argument, cleanupBindings.cleanupFunctionNames, cleanupBindings.subscriptionNames, { allowOpaqueReturn: true })) didFindCleanupReturn = true;
4698
4720
  });
4699
4721
  return didFindCleanupReturn;
4700
4722
  };
@@ -4710,7 +4732,7 @@ const effectNeedsCleanup = defineRule({
4710
4732
  if (!callback) return;
4711
4733
  const usages = findSubscribeLikeUsages(callback);
4712
4734
  if (usages.length === 0) return;
4713
- if (effectHasCleanupRelease(callback)) return;
4735
+ if (effectHasCleanupReturn(callback, usages)) return;
4714
4736
  const firstUsage = usages[0];
4715
4737
  const verb = firstUsage.kind === "timer" ? "schedules" : "subscribes via";
4716
4738
  context.report({
@@ -20179,6 +20201,12 @@ const getReactDoctorStringSetting = (settings, settingName) => {
20179
20201
  const settingValue = readOwnPropertyValue(bag, settingName);
20180
20202
  return typeof settingValue === "string" ? settingValue : void 0;
20181
20203
  };
20204
+ const getReactDoctorNumberSetting = (settings, settingName) => {
20205
+ const bag = readReactDoctorSettingsBag(settings);
20206
+ if (!bag) return void 0;
20207
+ const settingValue = readOwnPropertyValue(bag, settingName);
20208
+ return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
20209
+ };
20182
20210
  const getReactDoctorStringArraySetting = (settings, settingName) => {
20183
20211
  const bag = readReactDoctorSettingsBag(settings);
20184
20212
  if (!bag) return [];
@@ -27052,7 +27080,6 @@ const LEGACY_EXPO_PACKAGE_REPLACEMENTS = new Map([
27052
27080
  ["expo-av", "expo-audio for audio and expo-video for video"],
27053
27081
  ["expo-permissions", "the permissions API in each module (e.g. Camera.requestPermissionsAsync())"],
27054
27082
  ["expo-app-loading", "expo-splash-screen"],
27055
- ["expo-linear-gradient", "the `backgroundImage` CSS gradient style prop (New Architecture) or expo-linear-gradient's successor"],
27056
27083
  ["react-native-fast-image", "expo-image (drop-in with caching, placeholders, and crossfades)"]
27057
27084
  ]);
27058
27085
  const EXPO_UI_MODULE_SOURCES = new Set([
@@ -27207,6 +27234,10 @@ const RECYCLABLE_LIST_PACKAGES = {
27207
27234
  LegendList: ["@legendapp/list"]
27208
27235
  };
27209
27236
  const SIZING_HINT_ATTRIBUTE_NAMES = new Set(["estimatedItemSize", "estimatedListSize"]);
27237
+ const isFlashListV2OrNewer = (context) => {
27238
+ const flashListMajorVersion = getReactDoctorNumberSetting(context.settings, "shopifyFlashListMajorVersion");
27239
+ return flashListMajorVersion !== void 0 && flashListMajorVersion >= 2;
27240
+ };
27210
27241
  const isEmptyArrayLiteral = (node) => {
27211
27242
  if (!isNodeOfType(node.value, "JSXExpressionContainer")) return false;
27212
27243
  const expression = node.value.expression;
@@ -27228,6 +27259,7 @@ const rnListMissingEstimatedItemSize = defineRule({
27228
27259
  break;
27229
27260
  }
27230
27261
  if (!canonicalRecyclerName) return;
27262
+ if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
27231
27263
  let hasSizingHint = false;
27232
27264
  let dataIsEmptyLiteral = false;
27233
27265
  let hasDataProp = false;
@@ -28170,37 +28202,22 @@ const rnNoSingleElementStyleArray = defineRule({
28170
28202
  } })
28171
28203
  });
28172
28204
  //#endregion
28173
- //#region src/plugin/rules/react-native/utils/scrollview_names.ts
28174
- const SCROLLVIEW_NAMES = new Set([
28175
- "ScrollView",
28176
- "FlatList",
28177
- "SectionList",
28178
- "VirtualizedList",
28179
- "KeyboardAwareScrollView"
28180
- ]);
28205
+ //#region src/plugin/utils/define-retired-rule.ts
28206
+ const defineRetiredRule = (rule) => defineRule({
28207
+ ...rule,
28208
+ defaultEnabled: false,
28209
+ lifecycle: "retired",
28210
+ create: () => ({})
28211
+ });
28181
28212
  //#endregion
28182
28213
  //#region src/plugin/rules/react-native/rn-prefer-content-inset-adjustment.ts
28183
- const rnPreferContentInsetAdjustment = defineRule({
28214
+ const rnPreferContentInsetAdjustment = defineRetiredRule({
28184
28215
  id: "rn-prefer-content-inset-adjustment",
28185
- title: "SafeAreaView instead of contentInsetAdjustment",
28216
+ title: "Manual safe-area inset adjustment",
28186
28217
  tags: ["test-noise"],
28187
28218
  requires: ["react-native"],
28188
28219
  severity: "warn",
28189
- recommendation: "Drop the SafeAreaView wrapper and set `contentInsetAdjustmentBehavior=\"automatic\"` on the ScrollView so the OS handles the safe area.",
28190
- create: (context) => ({ JSXElement(node) {
28191
- if (resolveJsxElementName(node.openingElement) !== "SafeAreaView") return;
28192
- for (const child of node.children ?? []) {
28193
- if (!isNodeOfType(child, "JSXElement")) continue;
28194
- const childName = resolveJsxElementName(child.openingElement);
28195
- if (!childName || !SCROLLVIEW_NAMES.has(childName)) continue;
28196
- if (childName === "KeyboardAwareScrollView") continue;
28197
- context.report({
28198
- node,
28199
- message: `Your users render an extra wrapper view from <SafeAreaView> around <${childName}>.`
28200
- });
28201
- return;
28202
- }
28203
- } })
28220
+ recommendation: "Prefer native content inset adjustment only when it replaces manual inset plumbing; SafeAreaView wrappers are valid and intentionally ignored."
28204
28221
  });
28205
28222
  //#endregion
28206
28223
  //#region src/react-native-dependency-names.ts
@@ -28586,6 +28603,15 @@ const rnPressableSharedValueMutation = defineRule({
28586
28603
  }
28587
28604
  });
28588
28605
  //#endregion
28606
+ //#region src/plugin/rules/react-native/utils/scrollview_names.ts
28607
+ const SCROLLVIEW_NAMES = new Set([
28608
+ "ScrollView",
28609
+ "FlatList",
28610
+ "SectionList",
28611
+ "VirtualizedList",
28612
+ "KeyboardAwareScrollView"
28613
+ ]);
28614
+ //#endregion
28589
28615
  //#region src/plugin/rules/react-native/rn-scrollview-dynamic-padding.ts
28590
28616
  const rnScrollviewDynamicPadding = defineRule({
28591
28617
  id: "rn-scrollview-dynamic-padding",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.2.18-dev.8691177",
3
+ "version": "0.2.18-dev.87440ab",
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",