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