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.d.ts +657 -0
- package/dist/index.js +57 -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({
|
|
@@ -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/
|
|
28174
|
-
const
|
|
28175
|
-
|
|
28176
|
-
|
|
28177
|
-
"
|
|
28178
|
-
|
|
28179
|
-
|
|
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 =
|
|
28214
|
+
const rnPreferContentInsetAdjustment = defineRetiredRule({
|
|
28184
28215
|
id: "rn-prefer-content-inset-adjustment",
|
|
28185
|
-
title: "
|
|
28216
|
+
title: "Manual safe-area inset adjustment",
|
|
28186
28217
|
tags: ["test-noise"],
|
|
28187
28218
|
requires: ["react-native"],
|
|
28188
28219
|
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
|
-
} })
|
|
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.
|
|
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",
|