oxlint-plugin-react-doctor 0.2.18-dev.0a36216 → 0.2.18-dev.3c05fc4
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.d.ts +657 -0
- package/dist/index.js +58 -31
- package/package.json +1 -1
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, "
|
|
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
|
|
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 (
|
|
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 (
|
|
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 [];
|
|
@@ -27052,7 +27081,6 @@ const LEGACY_EXPO_PACKAGE_REPLACEMENTS = new Map([
|
|
|
27052
27081
|
["expo-av", "expo-audio for audio and expo-video for video"],
|
|
27053
27082
|
["expo-permissions", "the permissions API in each module (e.g. Camera.requestPermissionsAsync())"],
|
|
27054
27083
|
["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
27084
|
["react-native-fast-image", "expo-image (drop-in with caching, placeholders, and crossfades)"]
|
|
27057
27085
|
]);
|
|
27058
27086
|
const EXPO_UI_MODULE_SOURCES = new Set([
|
|
@@ -27207,6 +27235,10 @@ const RECYCLABLE_LIST_PACKAGES = {
|
|
|
27207
27235
|
LegendList: ["@legendapp/list"]
|
|
27208
27236
|
};
|
|
27209
27237
|
const SIZING_HINT_ATTRIBUTE_NAMES = new Set(["estimatedItemSize", "estimatedListSize"]);
|
|
27238
|
+
const isFlashListV2OrNewer = (context) => {
|
|
27239
|
+
const flashListMajorVersion = getReactDoctorNumberSetting(context.settings, "shopifyFlashListMajorVersion");
|
|
27240
|
+
return flashListMajorVersion !== void 0 && flashListMajorVersion >= 2;
|
|
27241
|
+
};
|
|
27210
27242
|
const isEmptyArrayLiteral = (node) => {
|
|
27211
27243
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return false;
|
|
27212
27244
|
const expression = node.value.expression;
|
|
@@ -27228,6 +27260,7 @@ const rnListMissingEstimatedItemSize = defineRule({
|
|
|
27228
27260
|
break;
|
|
27229
27261
|
}
|
|
27230
27262
|
if (!canonicalRecyclerName) return;
|
|
27263
|
+
if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
|
|
27231
27264
|
let hasSizingHint = false;
|
|
27232
27265
|
let dataIsEmptyLiteral = false;
|
|
27233
27266
|
let hasDataProp = false;
|
|
@@ -28170,37 +28203,22 @@ const rnNoSingleElementStyleArray = defineRule({
|
|
|
28170
28203
|
} })
|
|
28171
28204
|
});
|
|
28172
28205
|
//#endregion
|
|
28173
|
-
//#region src/plugin/
|
|
28174
|
-
const
|
|
28175
|
-
|
|
28176
|
-
|
|
28177
|
-
"
|
|
28178
|
-
|
|
28179
|
-
|
|
28180
|
-
]);
|
|
28206
|
+
//#region src/plugin/utils/define-retired-rule.ts
|
|
28207
|
+
const defineRetiredRule = (rule) => defineRule({
|
|
28208
|
+
...rule,
|
|
28209
|
+
defaultEnabled: false,
|
|
28210
|
+
lifecycle: "retired",
|
|
28211
|
+
create: () => ({})
|
|
28212
|
+
});
|
|
28181
28213
|
//#endregion
|
|
28182
28214
|
//#region src/plugin/rules/react-native/rn-prefer-content-inset-adjustment.ts
|
|
28183
|
-
const rnPreferContentInsetAdjustment =
|
|
28215
|
+
const rnPreferContentInsetAdjustment = defineRetiredRule({
|
|
28184
28216
|
id: "rn-prefer-content-inset-adjustment",
|
|
28185
|
-
title: "
|
|
28217
|
+
title: "Manual safe-area inset adjustment",
|
|
28186
28218
|
tags: ["test-noise"],
|
|
28187
28219
|
requires: ["react-native"],
|
|
28188
28220
|
severity: "warn",
|
|
28189
|
-
recommendation: "
|
|
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
|
-
} })
|
|
28221
|
+
recommendation: "Prefer native content inset adjustment only when it replaces manual inset plumbing; SafeAreaView wrappers are valid and intentionally ignored."
|
|
28204
28222
|
});
|
|
28205
28223
|
//#endregion
|
|
28206
28224
|
//#region src/react-native-dependency-names.ts
|
|
@@ -28586,6 +28604,15 @@ const rnPressableSharedValueMutation = defineRule({
|
|
|
28586
28604
|
}
|
|
28587
28605
|
});
|
|
28588
28606
|
//#endregion
|
|
28607
|
+
//#region src/plugin/rules/react-native/utils/scrollview_names.ts
|
|
28608
|
+
const SCROLLVIEW_NAMES = new Set([
|
|
28609
|
+
"ScrollView",
|
|
28610
|
+
"FlatList",
|
|
28611
|
+
"SectionList",
|
|
28612
|
+
"VirtualizedList",
|
|
28613
|
+
"KeyboardAwareScrollView"
|
|
28614
|
+
]);
|
|
28615
|
+
//#endregion
|
|
28589
28616
|
//#region src/plugin/rules/react-native/rn-scrollview-dynamic-padding.ts
|
|
28590
28617
|
const rnScrollviewDynamicPadding = defineRule({
|
|
28591
28618
|
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.
|
|
3
|
+
"version": "0.2.18-dev.3c05fc4",
|
|
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",
|