oxlint-plugin-react-doctor 0.2.18-dev.8d13ba3 → 0.2.18-dev.8e7fb33

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({
@@ -9703,6 +9725,7 @@ const jsxNoConstructedContextValues = defineRule({
9703
9725
  title: "Unstable context provider value",
9704
9726
  tags: ["react-jsx-only"],
9705
9727
  severity: "warn",
9728
+ disabledBy: ["react-compiler"],
9706
9729
  recommendation: "Wrap the context value in `useMemo`, or move it outside the component.",
9707
9730
  category: "Performance",
9708
9731
  create: (context) => {
@@ -20179,6 +20202,12 @@ const getReactDoctorStringSetting = (settings, settingName) => {
20179
20202
  const settingValue = readOwnPropertyValue(bag, settingName);
20180
20203
  return typeof settingValue === "string" ? settingValue : void 0;
20181
20204
  };
20205
+ const getReactDoctorNumberSetting = (settings, settingName) => {
20206
+ const bag = readReactDoctorSettingsBag(settings);
20207
+ if (!bag) return void 0;
20208
+ const settingValue = readOwnPropertyValue(bag, settingName);
20209
+ return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
20210
+ };
20182
20211
  const getReactDoctorStringArraySetting = (settings, settingName) => {
20183
20212
  const bag = readReactDoctorSettingsBag(settings);
20184
20213
  if (!bag) return [];
@@ -24822,6 +24851,7 @@ const preferModuleScopeStaticValue = defineRule({
24822
24851
  tags: ["test-noise"],
24823
24852
  severity: "warn",
24824
24853
  category: "Architecture",
24854
+ disabledBy: ["react-compiler"],
24825
24855
  recommendation: "Move the value above the component, at the top of the file. It doesn't use local state, so rebuilding it each update is wasted and makes it look new every time.",
24826
24856
  create: (context) => ({ VariableDeclarator(node) {
24827
24857
  if (!isNodeOfType(node.id, "Identifier")) return;
@@ -27052,7 +27082,6 @@ const LEGACY_EXPO_PACKAGE_REPLACEMENTS = new Map([
27052
27082
  ["expo-av", "expo-audio for audio and expo-video for video"],
27053
27083
  ["expo-permissions", "the permissions API in each module (e.g. Camera.requestPermissionsAsync())"],
27054
27084
  ["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
27085
  ["react-native-fast-image", "expo-image (drop-in with caching, placeholders, and crossfades)"]
27057
27086
  ]);
27058
27087
  const EXPO_UI_MODULE_SOURCES = new Set([
@@ -27207,6 +27236,10 @@ const RECYCLABLE_LIST_PACKAGES = {
27207
27236
  LegendList: ["@legendapp/list"]
27208
27237
  };
27209
27238
  const SIZING_HINT_ATTRIBUTE_NAMES = new Set(["estimatedItemSize", "estimatedListSize"]);
27239
+ const isFlashListV2OrNewer = (context) => {
27240
+ const flashListMajorVersion = getReactDoctorNumberSetting(context.settings, "shopifyFlashListMajorVersion");
27241
+ return flashListMajorVersion !== void 0 && flashListMajorVersion >= 2;
27242
+ };
27210
27243
  const isEmptyArrayLiteral = (node) => {
27211
27244
  if (!isNodeOfType(node.value, "JSXExpressionContainer")) return false;
27212
27245
  const expression = node.value.expression;
@@ -27228,6 +27261,7 @@ const rnListMissingEstimatedItemSize = defineRule({
27228
27261
  break;
27229
27262
  }
27230
27263
  if (!canonicalRecyclerName) return;
27264
+ if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
27231
27265
  let hasSizingHint = false;
27232
27266
  let dataIsEmptyLiteral = false;
27233
27267
  let hasDataProp = false;
@@ -28170,37 +28204,22 @@ const rnNoSingleElementStyleArray = defineRule({
28170
28204
  } })
28171
28205
  });
28172
28206
  //#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
- ]);
28207
+ //#region src/plugin/utils/define-retired-rule.ts
28208
+ const defineRetiredRule = (rule) => defineRule({
28209
+ ...rule,
28210
+ defaultEnabled: false,
28211
+ lifecycle: "retired",
28212
+ create: () => ({})
28213
+ });
28181
28214
  //#endregion
28182
28215
  //#region src/plugin/rules/react-native/rn-prefer-content-inset-adjustment.ts
28183
- const rnPreferContentInsetAdjustment = defineRule({
28216
+ const rnPreferContentInsetAdjustment = defineRetiredRule({
28184
28217
  id: "rn-prefer-content-inset-adjustment",
28185
- title: "SafeAreaView instead of contentInsetAdjustment",
28218
+ title: "Manual safe-area inset adjustment",
28186
28219
  tags: ["test-noise"],
28187
28220
  requires: ["react-native"],
28188
28221
  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
- } })
28222
+ recommendation: "Prefer native content inset adjustment only when it replaces manual inset plumbing; SafeAreaView wrappers are valid and intentionally ignored."
28204
28223
  });
28205
28224
  //#endregion
28206
28225
  //#region src/react-native-dependency-names.ts
@@ -28586,6 +28605,15 @@ const rnPressableSharedValueMutation = defineRule({
28586
28605
  }
28587
28606
  });
28588
28607
  //#endregion
28608
+ //#region src/plugin/rules/react-native/utils/scrollview_names.ts
28609
+ const SCROLLVIEW_NAMES = new Set([
28610
+ "ScrollView",
28611
+ "FlatList",
28612
+ "SectionList",
28613
+ "VirtualizedList",
28614
+ "KeyboardAwareScrollView"
28615
+ ]);
28616
+ //#endregion
28589
28617
  //#region src/plugin/rules/react-native/rn-scrollview-dynamic-padding.ts
28590
28618
  const rnScrollviewDynamicPadding = defineRule({
28591
28619
  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.8d13ba3",
3
+ "version": "0.2.18-dev.8e7fb33",
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",