oxlint-plugin-react-doctor 0.2.18-dev.8d13ba3 → 0.2.18-dev.abbb042
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 +93 -97
- 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 [];
|
|
@@ -23651,6 +23680,35 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
23651
23680
|
});
|
|
23652
23681
|
//#endregion
|
|
23653
23682
|
//#region src/plugin/rules/design/no-wide-letter-spacing.ts
|
|
23683
|
+
const getJsxAttributeStringValue = (attributeValue) => {
|
|
23684
|
+
if (!attributeValue) return null;
|
|
23685
|
+
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return attributeValue.value;
|
|
23686
|
+
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
23687
|
+
const expression = attributeValue.expression;
|
|
23688
|
+
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
|
|
23689
|
+
}
|
|
23690
|
+
return null;
|
|
23691
|
+
};
|
|
23692
|
+
const isTruthyBooleanJsxAttribute = (attributeValue) => {
|
|
23693
|
+
if (attributeValue === null) return true;
|
|
23694
|
+
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
23695
|
+
const expression = attributeValue.expression;
|
|
23696
|
+
return isNodeOfType(expression, "Literal") && expression.value === true;
|
|
23697
|
+
}
|
|
23698
|
+
return false;
|
|
23699
|
+
};
|
|
23700
|
+
const hasUppercaseSiblingProp = (styleAttribute) => {
|
|
23701
|
+
const openingElement = styleAttribute.parent;
|
|
23702
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
23703
|
+
for (const attribute of openingElement.attributes ?? []) {
|
|
23704
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
23705
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
23706
|
+
const attributeName = attribute.name.name;
|
|
23707
|
+
if (attributeName === "uppercase" && isTruthyBooleanJsxAttribute(attribute.value)) return true;
|
|
23708
|
+
if (attributeName === "textTransform" && getJsxAttributeStringValue(attribute.value) === "uppercase") return true;
|
|
23709
|
+
}
|
|
23710
|
+
return false;
|
|
23711
|
+
};
|
|
23654
23712
|
const noWideLetterSpacing = defineRule({
|
|
23655
23713
|
id: "no-wide-letter-spacing",
|
|
23656
23714
|
title: "Wide letter spacing on body text",
|
|
@@ -23660,6 +23718,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
23660
23718
|
create: (context) => ({ JSXAttribute(node) {
|
|
23661
23719
|
const expression = getInlineStyleExpression(node);
|
|
23662
23720
|
if (!expression) return;
|
|
23721
|
+
if (hasUppercaseSiblingProp(node)) return;
|
|
23663
23722
|
let isUppercase = false;
|
|
23664
23723
|
let letterSpacingProperty = null;
|
|
23665
23724
|
let letterSpacingEm = null;
|
|
@@ -24822,6 +24881,7 @@ const preferModuleScopeStaticValue = defineRule({
|
|
|
24822
24881
|
tags: ["test-noise"],
|
|
24823
24882
|
severity: "warn",
|
|
24824
24883
|
category: "Architecture",
|
|
24884
|
+
disabledBy: ["react-compiler"],
|
|
24825
24885
|
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
24886
|
create: (context) => ({ VariableDeclarator(node) {
|
|
24827
24887
|
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
@@ -26778,76 +26838,22 @@ const rerenderTransitionsScroll = defineRule({
|
|
|
26778
26838
|
} })
|
|
26779
26839
|
});
|
|
26780
26840
|
//#endregion
|
|
26841
|
+
//#region src/plugin/utils/define-retired-rule.ts
|
|
26842
|
+
const defineRetiredRule = (rule) => defineRule({
|
|
26843
|
+
...rule,
|
|
26844
|
+
defaultEnabled: false,
|
|
26845
|
+
lifecycle: "retired",
|
|
26846
|
+
create: () => ({})
|
|
26847
|
+
});
|
|
26848
|
+
//#endregion
|
|
26781
26849
|
//#region src/plugin/rules/react-native/rn-animate-layout-property.ts
|
|
26782
|
-
const
|
|
26783
|
-
"width",
|
|
26784
|
-
"height",
|
|
26785
|
-
"top",
|
|
26786
|
-
"left",
|
|
26787
|
-
"right",
|
|
26788
|
-
"bottom",
|
|
26789
|
-
"minWidth",
|
|
26790
|
-
"minHeight",
|
|
26791
|
-
"maxWidth",
|
|
26792
|
-
"maxHeight",
|
|
26793
|
-
"margin",
|
|
26794
|
-
"marginTop",
|
|
26795
|
-
"marginBottom",
|
|
26796
|
-
"marginLeft",
|
|
26797
|
-
"marginRight",
|
|
26798
|
-
"marginHorizontal",
|
|
26799
|
-
"marginVertical",
|
|
26800
|
-
"padding",
|
|
26801
|
-
"paddingTop",
|
|
26802
|
-
"paddingBottom",
|
|
26803
|
-
"paddingLeft",
|
|
26804
|
-
"paddingRight",
|
|
26805
|
-
"paddingHorizontal",
|
|
26806
|
-
"paddingVertical",
|
|
26807
|
-
"flex",
|
|
26808
|
-
"flexBasis",
|
|
26809
|
-
"flexGrow",
|
|
26810
|
-
"flexShrink",
|
|
26811
|
-
"borderWidth",
|
|
26812
|
-
"borderTopWidth",
|
|
26813
|
-
"borderBottomWidth",
|
|
26814
|
-
"borderLeftWidth",
|
|
26815
|
-
"borderRightWidth",
|
|
26816
|
-
"fontSize",
|
|
26817
|
-
"lineHeight",
|
|
26818
|
-
"letterSpacing"
|
|
26819
|
-
]);
|
|
26820
|
-
const findReturnedObject = (callback) => {
|
|
26821
|
-
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return null;
|
|
26822
|
-
const body = callback.body;
|
|
26823
|
-
if (isNodeOfType(body, "ObjectExpression")) return body;
|
|
26824
|
-
if (!isNodeOfType(body, "BlockStatement")) return null;
|
|
26825
|
-
for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isNodeOfType(stmt.argument, "ObjectExpression")) return stmt.argument;
|
|
26826
|
-
return null;
|
|
26827
|
-
};
|
|
26828
|
-
const rnAnimateLayoutProperty = defineRule({
|
|
26850
|
+
const rnAnimateLayoutProperty = defineRetiredRule({
|
|
26829
26851
|
id: "rn-animate-layout-property",
|
|
26830
26852
|
title: "Animating a layout property",
|
|
26831
26853
|
tags: ["test-noise"],
|
|
26832
26854
|
requires: ["react-native"],
|
|
26833
|
-
severity: "
|
|
26834
|
-
recommendation: "
|
|
26835
|
-
create: (context) => ({ CallExpression(node) {
|
|
26836
|
-
if (!isNodeOfType(node.callee, "Identifier") || node.callee.name !== "useAnimatedStyle") return;
|
|
26837
|
-
const callback = node.arguments?.[0];
|
|
26838
|
-
if (!callback) return;
|
|
26839
|
-
const returnedObject = findReturnedObject(callback);
|
|
26840
|
-
if (!returnedObject) return;
|
|
26841
|
-
for (const property of returnedObject.properties ?? []) {
|
|
26842
|
-
if (!isNodeOfType(property, "Property")) continue;
|
|
26843
|
-
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
26844
|
-
if (!REANIMATED_LAYOUT_KEYS.has(property.key.name)) continue;
|
|
26845
|
-
context.report({
|
|
26846
|
-
node: property,
|
|
26847
|
-
message: `Your users see stutter when useAnimatedStyle animates "${property.key.name}" on the layout thread.`
|
|
26848
|
-
});
|
|
26849
|
-
}
|
|
26850
|
-
} })
|
|
26855
|
+
severity: "warn",
|
|
26856
|
+
recommendation: "Reanimated useAnimatedStyle runs on the UI thread; layout-affecting properties driven by animation helpers, interpolate, or shared values are valid."
|
|
26851
26857
|
});
|
|
26852
26858
|
//#endregion
|
|
26853
26859
|
//#region src/plugin/rules/react-native/rn-animation-reaction-as-derived.ts
|
|
@@ -27052,7 +27058,6 @@ const LEGACY_EXPO_PACKAGE_REPLACEMENTS = new Map([
|
|
|
27052
27058
|
["expo-av", "expo-audio for audio and expo-video for video"],
|
|
27053
27059
|
["expo-permissions", "the permissions API in each module (e.g. Camera.requestPermissionsAsync())"],
|
|
27054
27060
|
["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
27061
|
["react-native-fast-image", "expo-image (drop-in with caching, placeholders, and crossfades)"]
|
|
27057
27062
|
]);
|
|
27058
27063
|
const EXPO_UI_MODULE_SOURCES = new Set([
|
|
@@ -27207,6 +27212,10 @@ const RECYCLABLE_LIST_PACKAGES = {
|
|
|
27207
27212
|
LegendList: ["@legendapp/list"]
|
|
27208
27213
|
};
|
|
27209
27214
|
const SIZING_HINT_ATTRIBUTE_NAMES = new Set(["estimatedItemSize", "estimatedListSize"]);
|
|
27215
|
+
const isFlashListV2OrNewer = (context) => {
|
|
27216
|
+
const flashListMajorVersion = getReactDoctorNumberSetting(context.settings, "shopifyFlashListMajorVersion");
|
|
27217
|
+
return flashListMajorVersion !== void 0 && flashListMajorVersion >= 2;
|
|
27218
|
+
};
|
|
27210
27219
|
const isEmptyArrayLiteral = (node) => {
|
|
27211
27220
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return false;
|
|
27212
27221
|
const expression = node.value.expression;
|
|
@@ -27228,6 +27237,7 @@ const rnListMissingEstimatedItemSize = defineRule({
|
|
|
27228
27237
|
break;
|
|
27229
27238
|
}
|
|
27230
27239
|
if (!canonicalRecyclerName) return;
|
|
27240
|
+
if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
|
|
27231
27241
|
let hasSizingHint = false;
|
|
27232
27242
|
let dataIsEmptyLiteral = false;
|
|
27233
27243
|
let hasDataProp = false;
|
|
@@ -28170,37 +28180,14 @@ const rnNoSingleElementStyleArray = defineRule({
|
|
|
28170
28180
|
} })
|
|
28171
28181
|
});
|
|
28172
28182
|
//#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
|
-
]);
|
|
28181
|
-
//#endregion
|
|
28182
28183
|
//#region src/plugin/rules/react-native/rn-prefer-content-inset-adjustment.ts
|
|
28183
|
-
const rnPreferContentInsetAdjustment =
|
|
28184
|
+
const rnPreferContentInsetAdjustment = defineRetiredRule({
|
|
28184
28185
|
id: "rn-prefer-content-inset-adjustment",
|
|
28185
|
-
title: "
|
|
28186
|
+
title: "Manual safe-area inset adjustment",
|
|
28186
28187
|
tags: ["test-noise"],
|
|
28187
28188
|
requires: ["react-native"],
|
|
28188
28189
|
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
|
-
} })
|
|
28190
|
+
recommendation: "Prefer native content inset adjustment only when it replaces manual inset plumbing; SafeAreaView wrappers are valid and intentionally ignored."
|
|
28204
28191
|
});
|
|
28205
28192
|
//#endregion
|
|
28206
28193
|
//#region src/react-native-dependency-names.ts
|
|
@@ -28586,6 +28573,15 @@ const rnPressableSharedValueMutation = defineRule({
|
|
|
28586
28573
|
}
|
|
28587
28574
|
});
|
|
28588
28575
|
//#endregion
|
|
28576
|
+
//#region src/plugin/rules/react-native/utils/scrollview_names.ts
|
|
28577
|
+
const SCROLLVIEW_NAMES = new Set([
|
|
28578
|
+
"ScrollView",
|
|
28579
|
+
"FlatList",
|
|
28580
|
+
"SectionList",
|
|
28581
|
+
"VirtualizedList",
|
|
28582
|
+
"KeyboardAwareScrollView"
|
|
28583
|
+
]);
|
|
28584
|
+
//#endregion
|
|
28589
28585
|
//#region src/plugin/rules/react-native/rn-scrollview-dynamic-padding.ts
|
|
28590
28586
|
const rnScrollviewDynamicPadding = defineRule({
|
|
28591
28587
|
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.abbb042",
|
|
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",
|