oxlint-plugin-react-doctor 0.7.2-dev.aa519e5 → 0.7.2-dev.cb8f726
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 +2350 -1563
- package/dist/index.js +324 -162
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -595,6 +595,19 @@ const TRIVIAL_INITIALIZER_NAMES = new Set([
|
|
|
595
595
|
"parseInt",
|
|
596
596
|
"parseFloat"
|
|
597
597
|
]);
|
|
598
|
+
const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
|
|
599
|
+
"Date",
|
|
600
|
+
"Map",
|
|
601
|
+
"Set",
|
|
602
|
+
"WeakMap",
|
|
603
|
+
"WeakSet",
|
|
604
|
+
"WeakRef",
|
|
605
|
+
"RegExp",
|
|
606
|
+
"Error",
|
|
607
|
+
"URL",
|
|
608
|
+
"URLSearchParams",
|
|
609
|
+
"AbortController"
|
|
610
|
+
]);
|
|
598
611
|
const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
|
|
599
612
|
"Boolean",
|
|
600
613
|
"String",
|
|
@@ -2235,6 +2248,38 @@ const anchorHasContent = defineRule({
|
|
|
2235
2248
|
}
|
|
2236
2249
|
});
|
|
2237
2250
|
//#endregion
|
|
2251
|
+
//#region src/plugin/utils/get-jsx-prop-static-string-values.ts
|
|
2252
|
+
const MAX_CONST_RESOLUTION_HOPS = 4;
|
|
2253
|
+
const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
|
|
2254
|
+
const expression = stripParenExpression(rawExpression);
|
|
2255
|
+
if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
|
|
2256
|
+
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2257
|
+
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2258
|
+
return staticValue === null ? null : [staticValue];
|
|
2259
|
+
}
|
|
2260
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
2261
|
+
const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
|
|
2262
|
+
if (consequentValues === null) return null;
|
|
2263
|
+
const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
|
|
2264
|
+
if (alternateValues === null) return null;
|
|
2265
|
+
return [...consequentValues, ...alternateValues];
|
|
2266
|
+
}
|
|
2267
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
2268
|
+
if (remainingHops === 0) return null;
|
|
2269
|
+
const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
|
|
2270
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
|
|
2271
|
+
return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
|
|
2272
|
+
}
|
|
2273
|
+
return null;
|
|
2274
|
+
};
|
|
2275
|
+
const getJsxPropStaticStringValues = (attribute, scopes) => {
|
|
2276
|
+
const value = attribute.value;
|
|
2277
|
+
if (!value) return null;
|
|
2278
|
+
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? [value.value] : null;
|
|
2279
|
+
if (isNodeOfType(value, "JSXExpressionContainer")) return resolveStaticStringValues(value.expression, scopes, MAX_CONST_RESOLUTION_HOPS);
|
|
2280
|
+
return null;
|
|
2281
|
+
};
|
|
2282
|
+
//#endregion
|
|
2238
2283
|
//#region src/plugin/rules/a11y/anchor-is-valid.ts
|
|
2239
2284
|
const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
|
|
2240
2285
|
const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
|
|
@@ -2264,28 +2309,11 @@ const isInvalidHref = (value, validHrefs) => {
|
|
|
2264
2309
|
if (validHrefs.has(value)) return false;
|
|
2265
2310
|
return value === "" || value === "#" || value === "javascript:void(0)";
|
|
2266
2311
|
};
|
|
2267
|
-
const
|
|
2268
|
-
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value : null;
|
|
2269
|
-
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2270
|
-
const expression = value.expression;
|
|
2271
|
-
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
|
|
2272
|
-
if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression);
|
|
2273
|
-
}
|
|
2274
|
-
return null;
|
|
2275
|
-
};
|
|
2276
|
-
const checkValueIsEmptyOrInvalid = (value, validHrefs) => {
|
|
2277
|
-
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
|
|
2312
|
+
const isNullishOrFragmentHref = (value) => {
|
|
2278
2313
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2279
2314
|
const expression = value.expression;
|
|
2280
2315
|
if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
|
|
2281
|
-
if (isNodeOfType(expression, "Literal"))
|
|
2282
|
-
if (expression.value === null) return true;
|
|
2283
|
-
if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
|
|
2284
|
-
}
|
|
2285
|
-
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2286
|
-
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2287
|
-
return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
|
|
2288
|
-
}
|
|
2316
|
+
if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
|
|
2289
2317
|
}
|
|
2290
2318
|
if (isNodeOfType(value, "JSXFragment")) return true;
|
|
2291
2319
|
return false;
|
|
@@ -2318,9 +2346,10 @@ const anchorIsValid = defineRule({
|
|
|
2318
2346
|
});
|
|
2319
2347
|
return;
|
|
2320
2348
|
}
|
|
2321
|
-
|
|
2349
|
+
const hrefCandidates = getJsxPropStaticStringValues(hrefAttribute, context.scopes);
|
|
2350
|
+
if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
|
|
2322
2351
|
const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
|
|
2323
|
-
if (!hasOnClick &&
|
|
2352
|
+
if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
|
|
2324
2353
|
context.report({
|
|
2325
2354
|
node: node.name,
|
|
2326
2355
|
message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
|
|
@@ -3376,6 +3405,25 @@ const ariaRole = defineRule({
|
|
|
3376
3405
|
if (!roleAttribute) return;
|
|
3377
3406
|
const elementType = getElementType(node, context.settings);
|
|
3378
3407
|
if (settings.ignoreNonDOM && !HTML_TAGS.has(elementType)) return;
|
|
3408
|
+
const reportFirstInvalidCandidate = (candidates) => {
|
|
3409
|
+
for (const candidate of candidates) {
|
|
3410
|
+
if (candidate.trim().length === 0) {
|
|
3411
|
+
context.report({
|
|
3412
|
+
node: roleAttribute,
|
|
3413
|
+
message: buildBaseMessage("")
|
|
3414
|
+
});
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
const tokens = candidate.split(/\s+/).filter((token) => token.length > 0);
|
|
3418
|
+
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3419
|
+
context.report({
|
|
3420
|
+
node: roleAttribute,
|
|
3421
|
+
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3422
|
+
});
|
|
3423
|
+
return;
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
};
|
|
3379
3427
|
const value = roleAttribute.value;
|
|
3380
3428
|
if (!value) {
|
|
3381
3429
|
context.report({
|
|
@@ -3392,22 +3440,7 @@ const ariaRole = defineRule({
|
|
|
3392
3440
|
});
|
|
3393
3441
|
return;
|
|
3394
3442
|
}
|
|
3395
|
-
|
|
3396
|
-
if (stringValue.trim().length === 0) {
|
|
3397
|
-
context.report({
|
|
3398
|
-
node: roleAttribute,
|
|
3399
|
-
message: buildBaseMessage("")
|
|
3400
|
-
});
|
|
3401
|
-
return;
|
|
3402
|
-
}
|
|
3403
|
-
const tokens = stringValue.split(/\s+/).filter((token) => token.length > 0);
|
|
3404
|
-
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3405
|
-
context.report({
|
|
3406
|
-
node: roleAttribute,
|
|
3407
|
-
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3408
|
-
});
|
|
3409
|
-
return;
|
|
3410
|
-
}
|
|
3443
|
+
reportFirstInvalidCandidate([value.value]);
|
|
3411
3444
|
return;
|
|
3412
3445
|
}
|
|
3413
3446
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
@@ -3428,6 +3461,11 @@ const ariaRole = defineRule({
|
|
|
3428
3461
|
});
|
|
3429
3462
|
return;
|
|
3430
3463
|
}
|
|
3464
|
+
const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
3465
|
+
if (resolvedCandidates !== null) {
|
|
3466
|
+
reportFirstInvalidCandidate(resolvedCandidates);
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3431
3469
|
return;
|
|
3432
3470
|
}
|
|
3433
3471
|
context.report({
|
|
@@ -9616,7 +9654,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9616
9654
|
});
|
|
9617
9655
|
//#endregion
|
|
9618
9656
|
//#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
|
|
9619
|
-
const EMPTY_VISITORS$
|
|
9657
|
+
const EMPTY_VISITORS$6 = {};
|
|
9620
9658
|
const NODE_OR_BUILD_FILE = /(\.config\.[cm]?[jt]sx?$)|((^|\/)(scripts|tools|tooling|cli|bin)\/)|(\+(api|html)\.[cm]?[jt]sx?$)|(\.server\.[cm]?[jt]sx?$)|(\.(test|spec)\.[cm]?[jt]sx?$)|((^|\/)__tests__\/)|(\.e2e\.[cm]?[jt]sx?$)/;
|
|
9621
9659
|
const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
|
|
9622
9660
|
const isProcessEnv = (node) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "process" && isNodeOfType(node.property, "Identifier") && node.property.name === "env";
|
|
@@ -9628,7 +9666,7 @@ const expoNoNonInlinedEnv = defineRule({
|
|
|
9628
9666
|
recommendation: "Read env vars with static dotted access (`process.env.EXPO_PUBLIC_NAME`). Computed access and destructuring aren't inlined by babel-preset-expo and resolve to `undefined` at runtime.",
|
|
9629
9667
|
create: (context) => {
|
|
9630
9668
|
const filename = normalizeFilename(context.filename ?? "");
|
|
9631
|
-
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$
|
|
9669
|
+
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$6;
|
|
9632
9670
|
return {
|
|
9633
9671
|
MemberExpression(node) {
|
|
9634
9672
|
if (!node.computed) return;
|
|
@@ -11046,8 +11084,8 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11046
11084
|
if (node.attributes.length === 0) return;
|
|
11047
11085
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
11048
11086
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
11049
|
-
const
|
|
11050
|
-
if (
|
|
11087
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
|
|
11088
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
11051
11089
|
let hasInteractiveHandler = false;
|
|
11052
11090
|
for (const attribute of node.attributes) {
|
|
11053
11091
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11061,11 +11099,16 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11061
11099
|
const elementType = getElementType(node, context.settings);
|
|
11062
11100
|
if (!HTML_TAGS.has(elementType)) return;
|
|
11063
11101
|
if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
11064
|
-
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11065
|
-
if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
|
|
11066
11102
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
11067
|
-
|
|
11068
|
-
const
|
|
11103
|
+
const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
|
|
11104
|
+
for (const role of roleCandidates) {
|
|
11105
|
+
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11106
|
+
if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
|
|
11107
|
+
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
11108
|
+
}
|
|
11109
|
+
const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
|
|
11110
|
+
const roleDisplay = roleCandidates.join("' / '");
|
|
11111
|
+
const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
|
|
11069
11112
|
context.report({
|
|
11070
11113
|
node,
|
|
11071
11114
|
message
|
|
@@ -13508,7 +13551,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
13508
13551
|
title: "Spread copy before sort()",
|
|
13509
13552
|
tags: ["test-noise"],
|
|
13510
13553
|
severity: "warn",
|
|
13511
|
-
|
|
13554
|
+
disabledWhen: ["react-native", "pre-es2023"],
|
|
13512
13555
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
13513
13556
|
create: (context) => ({ CallExpression(node) {
|
|
13514
13557
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
@@ -14755,7 +14798,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
14755
14798
|
title: "Unstable context provider value",
|
|
14756
14799
|
tags: ["react-jsx-only"],
|
|
14757
14800
|
severity: "warn",
|
|
14758
|
-
|
|
14801
|
+
disabledWhen: ["react-compiler"],
|
|
14759
14802
|
recommendation: "Wrap the context value in `useMemo` or move it outside the component so consumers do not redraw every render.",
|
|
14760
14803
|
category: "Performance",
|
|
14761
14804
|
create: (context) => {
|
|
@@ -15123,7 +15166,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
15123
15166
|
title: "JSX element passed as a prop",
|
|
15124
15167
|
tags: ["react-jsx-only"],
|
|
15125
15168
|
severity: "warn",
|
|
15126
|
-
|
|
15169
|
+
disabledWhen: ["react-compiler"],
|
|
15127
15170
|
recommendation: "Move the JSX outside the component or wrap it in `useMemo` so memoized children do not redraw every render.",
|
|
15128
15171
|
category: "Performance",
|
|
15129
15172
|
create: (context) => {
|
|
@@ -15511,7 +15554,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
15511
15554
|
title: "New array passed as a prop",
|
|
15512
15555
|
tags: ["react-jsx-only"],
|
|
15513
15556
|
severity: "warn",
|
|
15514
|
-
|
|
15557
|
+
disabledWhen: ["react-compiler"],
|
|
15515
15558
|
recommendation: "Wrap the array in `useMemo` or move it outside the component so memoized children do not redraw every render.",
|
|
15516
15559
|
category: "Performance",
|
|
15517
15560
|
create: (context) => {
|
|
@@ -15975,7 +16018,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
15975
16018
|
title: "New function passed as a prop",
|
|
15976
16019
|
tags: ["react-jsx-only"],
|
|
15977
16020
|
severity: "warn",
|
|
15978
|
-
|
|
16021
|
+
disabledWhen: ["react-compiler"],
|
|
15979
16022
|
recommendation: "Wrap the callback in `useCallback` or move it outside the component so memoized children do not redraw every render.",
|
|
15980
16023
|
category: "Performance",
|
|
15981
16024
|
create: (context) => {
|
|
@@ -16282,7 +16325,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
16282
16325
|
title: "New object passed as a prop",
|
|
16283
16326
|
tags: ["react-jsx-only"],
|
|
16284
16327
|
severity: "warn",
|
|
16285
|
-
|
|
16328
|
+
disabledWhen: ["react-compiler"],
|
|
16286
16329
|
recommendation: "Wrap the object in `useMemo` or move it outside the component so memoized children do not redraw every render.",
|
|
16287
16330
|
category: "Performance",
|
|
16288
16331
|
create: (context) => {
|
|
@@ -18106,6 +18149,7 @@ const getReactDoctorStringArraySetting = (settings, settingName) => {
|
|
|
18106
18149
|
if (!Array.isArray(settingValue)) return [];
|
|
18107
18150
|
return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
18108
18151
|
};
|
|
18152
|
+
const hasCapability = (settings, capability) => getReactDoctorStringArraySetting(settings, "capabilities").includes(capability);
|
|
18109
18153
|
//#endregion
|
|
18110
18154
|
//#region src/plugin/utils/is-in-project-directory.ts
|
|
18111
18155
|
const isInProjectDirectory = (context, directoryPath) => {
|
|
@@ -18560,6 +18604,7 @@ const describeClientSideNavigation = (node) => {
|
|
|
18560
18604
|
}
|
|
18561
18605
|
return null;
|
|
18562
18606
|
};
|
|
18607
|
+
const STATIC_EXPORT_RECOMMENDATION = "Avoid redirects inside useEffect — they flash the wrong page first. Use an event handler (e.g. onClick), or call redirect() from next/navigation during render (it prerenders a client-side redirect under output: \"export\"). Middleware and getServerSideProps redirects aren't available in a static export.";
|
|
18563
18608
|
const nextjsNoClientSideRedirect = defineRule({
|
|
18564
18609
|
id: "nextjs-no-client-side-redirect",
|
|
18565
18610
|
title: "Client-side redirect for navigation",
|
|
@@ -18567,6 +18612,7 @@ const nextjsNoClientSideRedirect = defineRule({
|
|
|
18567
18612
|
requires: ["nextjs"],
|
|
18568
18613
|
severity: "warn",
|
|
18569
18614
|
recommendation: "Avoid redirects inside useEffect. Use an event handler, middleware, or server-side redirect (App Router: redirect() from next/navigation; Pages Router: getServerSideProps redirect)",
|
|
18615
|
+
recommendationFor: (hasCapability) => hasCapability("nextjs:static-export") ? STATIC_EXPORT_RECOMMENDATION : void 0,
|
|
18570
18616
|
create: (context) => {
|
|
18571
18617
|
return { CallExpression(node) {
|
|
18572
18618
|
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
@@ -22474,6 +22520,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
|
|
|
22474
22520
|
const section = manifest[sectionName];
|
|
22475
22521
|
return typeof section === "object" && section !== null && Object.keys(section).length > 0;
|
|
22476
22522
|
});
|
|
22523
|
+
const declaresDependency = (manifest, dependencyName) => {
|
|
22524
|
+
for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
|
|
22525
|
+
return false;
|
|
22526
|
+
};
|
|
22477
22527
|
const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
|
|
22478
22528
|
const classifyPackagePlatform = (filename) => {
|
|
22479
22529
|
const manifest = readNearestPackageManifest(filename);
|
|
@@ -22490,9 +22540,7 @@ const classifyPackagePlatform = (filename) => {
|
|
|
22490
22540
|
return result;
|
|
22491
22541
|
};
|
|
22492
22542
|
//#endregion
|
|
22493
|
-
//#region src/plugin/utils/is-
|
|
22494
|
-
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22495
|
-
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22543
|
+
//#region src/plugin/utils/is-package-nested-below-project-root.ts
|
|
22496
22544
|
const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
|
|
22497
22545
|
const resolveRealDirectory = (directory) => {
|
|
22498
22546
|
const cached = cachedRealDirectoryByDirectory.get(directory);
|
|
@@ -22513,6 +22561,10 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
|
|
|
22513
22561
|
const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
|
|
22514
22562
|
return realPackageDirectory.startsWith(rootPrefix);
|
|
22515
22563
|
};
|
|
22564
|
+
//#endregion
|
|
22565
|
+
//#region src/plugin/utils/is-react-native-file.ts
|
|
22566
|
+
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22567
|
+
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22516
22568
|
const classifyReactNativeFileTarget = (context) => {
|
|
22517
22569
|
const rawFilename = context.filename;
|
|
22518
22570
|
if (!rawFilename) return "unknown";
|
|
@@ -22929,7 +22981,6 @@ const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
|
22929
22981
|
let fallThroughCount = 0;
|
|
22930
22982
|
let maxTerminatingPathCount = 0;
|
|
22931
22983
|
for (const statement of statements) {
|
|
22932
|
-
if (isFunctionLike$1(statement)) continue;
|
|
22933
22984
|
const guardBranch = isGuardWithTerminatingBranch(statement);
|
|
22934
22985
|
if (guardBranch) {
|
|
22935
22986
|
maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
|
|
@@ -22965,14 +23016,23 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
|
|
|
22965
23016
|
if (!isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
22966
23017
|
return parent.parent === effectCallback.body;
|
|
22967
23018
|
};
|
|
23019
|
+
const countFunctionBodySetStateCalls = (functionNode, context) => {
|
|
23020
|
+
if (isAsyncFunctionLike(functionNode)) return 0;
|
|
23021
|
+
const body = functionNode.body;
|
|
23022
|
+
if (!body) return 0;
|
|
23023
|
+
return countMaxPathSetStateCalls(body, context);
|
|
23024
|
+
};
|
|
22968
23025
|
const countMaxPathSetStateCalls = (node, context) => {
|
|
22969
23026
|
if (!node || typeof node !== "object") return 0;
|
|
22970
|
-
if (
|
|
23027
|
+
if (isFunctionLike$1(node)) return 0;
|
|
22971
23028
|
if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
|
|
22972
23029
|
if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
|
|
22973
23030
|
const consequent = node.consequent;
|
|
22974
23031
|
const alternate = node.alternate;
|
|
22975
|
-
|
|
23032
|
+
const testCount = countMaxPathSetStateCalls(node.test, context);
|
|
23033
|
+
const thenCount = countMaxPathSetStateCalls(consequent, context);
|
|
23034
|
+
const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
|
|
23035
|
+
return testCount + Math.max(thenCount, elseCount);
|
|
22976
23036
|
}
|
|
22977
23037
|
if (isNodeOfType(node, "SwitchStatement")) {
|
|
22978
23038
|
let maxRunSetters = 0;
|
|
@@ -23002,28 +23062,31 @@ const countMaxPathSetStateCalls = (node, context) => {
|
|
|
23002
23062
|
}
|
|
23003
23063
|
if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
|
|
23004
23064
|
let nestedSettersInArgs = 0;
|
|
23005
|
-
for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
|
|
23065
|
+
for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$1(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
|
|
23006
23066
|
return 1 + nestedSettersInArgs;
|
|
23007
23067
|
}
|
|
23008
23068
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
|
|
23009
23069
|
const helperFunction = context.helpersByName.get(node.callee.name);
|
|
23010
23070
|
if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
|
|
23011
23071
|
context.activeHelpers.add(helperFunction);
|
|
23012
|
-
let helperCount =
|
|
23072
|
+
let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
|
|
23013
23073
|
context.activeHelpers.delete(helperFunction);
|
|
23014
23074
|
for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
|
|
23015
23075
|
return helperCount;
|
|
23016
23076
|
}
|
|
23017
23077
|
}
|
|
23018
|
-
const
|
|
23078
|
+
const countChild = (child) => {
|
|
23079
|
+
if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
|
|
23080
|
+
return countMaxPathSetStateCalls(child, context);
|
|
23081
|
+
};
|
|
23019
23082
|
let total = 0;
|
|
23020
23083
|
const nodeRecord = node;
|
|
23021
23084
|
for (const key of Object.keys(nodeRecord)) {
|
|
23022
23085
|
if (key === "parent") continue;
|
|
23023
23086
|
const child = nodeRecord[key];
|
|
23024
23087
|
if (Array.isArray(child)) {
|
|
23025
|
-
for (const item of child) if (item && typeof item === "object" && "type" in item
|
|
23026
|
-
} else if (child && typeof child === "object" && "type" in child
|
|
23088
|
+
for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
|
|
23089
|
+
} else if (child && typeof child === "object" && "type" in child) total += countChild(child);
|
|
23027
23090
|
}
|
|
23028
23091
|
return total;
|
|
23029
23092
|
};
|
|
@@ -23072,7 +23135,7 @@ const noCascadingSetState = defineRule({
|
|
|
23072
23135
|
const callback = getEffectCallback(node);
|
|
23073
23136
|
if (!callback) return;
|
|
23074
23137
|
if (isDevOnlyGuardedEffect(callback)) return;
|
|
23075
|
-
const setStateCallCount =
|
|
23138
|
+
const setStateCallCount = countFunctionBodySetStateCalls(callback, {
|
|
23076
23139
|
helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
|
|
23077
23140
|
activeHelpers: /* @__PURE__ */ new Set(),
|
|
23078
23141
|
effectCallback: callback
|
|
@@ -26290,7 +26353,11 @@ const noEffectEventInDeps = defineRule({
|
|
|
26290
26353
|
const initializer = declaratorNode.init;
|
|
26291
26354
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
26292
26355
|
if (!isHookCall$2(initializer, "useEffectEvent")) return;
|
|
26293
|
-
if (isNodeOfType(initializer.callee, "Identifier")
|
|
26356
|
+
if (isNodeOfType(initializer.callee, "Identifier")) {
|
|
26357
|
+
if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
|
|
26358
|
+
const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
|
|
26359
|
+
if (calleeSymbol && calleeSymbol.kind !== "import") return;
|
|
26360
|
+
}
|
|
26294
26361
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26295
26362
|
} });
|
|
26296
26363
|
return {
|
|
@@ -28928,7 +28995,7 @@ const noJsonParseStringifyClone = defineRule({
|
|
|
28928
28995
|
id: "no-json-parse-stringify-clone",
|
|
28929
28996
|
title: "JSON parse/stringify deep clone",
|
|
28930
28997
|
severity: "warn",
|
|
28931
|
-
|
|
28998
|
+
disabledWhen: ["react-native"],
|
|
28932
28999
|
recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
|
|
28933
29000
|
create: (context) => ({ CallExpression(node) {
|
|
28934
29001
|
if (!isJsonMethodCall(node, "parse")) return;
|
|
@@ -31752,12 +31819,8 @@ const noPolymorphicChildren = defineRule({
|
|
|
31752
31819
|
//#endregion
|
|
31753
31820
|
//#region src/plugin/rules/correctness/no-prevent-default.ts
|
|
31754
31821
|
const PREVENT_DEFAULT_ELEMENTS = new Map([["form", ["onSubmit"]], ["a", ["onClick"]]]);
|
|
31755
|
-
const SERVER_CAPABLE_FRAMEWORKS = new Set([
|
|
31756
|
-
"nextjs",
|
|
31757
|
-
"tanstack-start",
|
|
31758
|
-
"remix"
|
|
31759
|
-
]);
|
|
31760
31822
|
const FORM_MESSAGE_SERVER_CAPABLE = "Your users can't submit this <form> without JavaScript because onSubmit calls preventDefault(), so use a server action like `<form action={serverAction}>` to make it work either way.";
|
|
31823
|
+
const FORM_MESSAGE_GENERIC = "Your users can't submit this <form> because onSubmit calls preventDefault().";
|
|
31761
31824
|
const ANCHOR_MESSAGE = "Your users click this <a> & nothing navigates because onClick calls preventDefault(), so use a <button> or a routing component instead.";
|
|
31762
31825
|
const collectPreventDefaultCalls = (node) => {
|
|
31763
31826
|
const preventDefaultCalls = [];
|
|
@@ -31894,15 +31957,18 @@ const noPreventDefault = defineRule({
|
|
|
31894
31957
|
recommendation: "Use `<form action>` where your framework supports it (it works without JS), or use a `<button>` instead of an `<a>` with preventDefault.",
|
|
31895
31958
|
create: (context) => {
|
|
31896
31959
|
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
31897
|
-
const
|
|
31960
|
+
const isClientOnlyFramework = hasCapability(context.settings, "client-only");
|
|
31961
|
+
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
31962
|
+
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
31898
31963
|
return { JSXOpeningElement(node) {
|
|
31899
31964
|
const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
|
|
31900
31965
|
if (!elementName) return;
|
|
31901
31966
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
31902
31967
|
if (!targetEventProps) return;
|
|
31903
31968
|
if (elementName === "form") {
|
|
31904
|
-
if (
|
|
31905
|
-
if (framework === "
|
|
31969
|
+
if (isClientOnlyFramework) return;
|
|
31970
|
+
if (framework === void 0 || framework === "unknown") return;
|
|
31971
|
+
if (framework === "nextjs" && isServerActionsFramework) {
|
|
31906
31972
|
const programRoot = findProgramRoot(node);
|
|
31907
31973
|
if (!programRoot || !hasDirective(programRoot, "use client")) return;
|
|
31908
31974
|
}
|
|
@@ -31926,7 +31992,7 @@ const noPreventDefault = defineRule({
|
|
|
31926
31992
|
}
|
|
31927
31993
|
context.report({
|
|
31928
31994
|
node,
|
|
31929
|
-
message: elementName === "form" ?
|
|
31995
|
+
message: elementName === "form" ? formMessage : ANCHOR_MESSAGE
|
|
31930
31996
|
});
|
|
31931
31997
|
return;
|
|
31932
31998
|
}
|
|
@@ -33196,11 +33262,41 @@ const isIdentifierLikeKeyNameValue = (literalValue) => {
|
|
|
33196
33262
|
if (wordSegments.length < 2) return false;
|
|
33197
33263
|
return wordSegments.every((segment) => /^[a-z]+$/.test(segment));
|
|
33198
33264
|
};
|
|
33265
|
+
const FRAMEWORK_ENV_ADVICE = [
|
|
33266
|
+
[
|
|
33267
|
+
"nextjs",
|
|
33268
|
+
"Next.js",
|
|
33269
|
+
"NEXT_PUBLIC_*"
|
|
33270
|
+
],
|
|
33271
|
+
[
|
|
33272
|
+
"vite",
|
|
33273
|
+
"Vite",
|
|
33274
|
+
"VITE_*"
|
|
33275
|
+
],
|
|
33276
|
+
[
|
|
33277
|
+
"tanstack-start",
|
|
33278
|
+
"TanStack Start",
|
|
33279
|
+
"VITE_*"
|
|
33280
|
+
],
|
|
33281
|
+
[
|
|
33282
|
+
"cra",
|
|
33283
|
+
"Create React App",
|
|
33284
|
+
"REACT_APP_*"
|
|
33285
|
+
],
|
|
33286
|
+
[
|
|
33287
|
+
"gatsby",
|
|
33288
|
+
"Gatsby",
|
|
33289
|
+
"GATSBY_*"
|
|
33290
|
+
]
|
|
33291
|
+
];
|
|
33199
33292
|
const noSecretsInClientCode = defineRule({
|
|
33200
33293
|
id: "no-secrets-in-client-code",
|
|
33201
33294
|
title: "Secret in client code",
|
|
33202
33295
|
severity: "warn",
|
|
33203
33296
|
recommendation: "Move secrets to server-only code. Anything in client env variables gets shipped to the browser, so it can't hold secrets.",
|
|
33297
|
+
recommendationFor: (hasCapability) => {
|
|
33298
|
+
for (const [frameworkToken, frameworkName, publicEnvPrefix] of FRAMEWORK_ENV_ADVICE) if (hasCapability(frameworkToken)) return `Move secrets to server-only code. In ${frameworkName}, only \`${publicEnvPrefix}\` env vars are exposed to the browser, and they must not contain secrets`;
|
|
33299
|
+
},
|
|
33204
33300
|
create: (context) => {
|
|
33205
33301
|
const filename = normalizeFilename(context.filename ?? "");
|
|
33206
33302
|
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
@@ -33238,7 +33334,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
33238
33334
|
const isServerOnlyScope = isInsideServerOnlyScope(node);
|
|
33239
33335
|
const trailingSuffix = getIdentifierTrailingWord(variableName);
|
|
33240
33336
|
const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
|
|
33241
|
-
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
|
|
33337
|
+
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
|
|
33242
33338
|
context.report({
|
|
33243
33339
|
node,
|
|
33244
33340
|
message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
|
|
@@ -36543,8 +36639,7 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
|
|
|
36543
36639
|
const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
|
|
36544
36640
|
const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
|
|
36545
36641
|
const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
|
|
36546
|
-
const
|
|
36547
|
-
const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
|
|
36642
|
+
const NAMESPACE_OBJECT_MESSAGE = "This export bundles components inside an object, so Fast Refresh can't track them and falls back to a full reload.";
|
|
36548
36643
|
const DEFAULT_REACT_HOCS = [
|
|
36549
36644
|
"memo",
|
|
36550
36645
|
"forwardRef",
|
|
@@ -36615,6 +36710,20 @@ const isReactComponentInitializer = (expression, state) => {
|
|
|
36615
36710
|
if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
|
|
36616
36711
|
return false;
|
|
36617
36712
|
};
|
|
36713
|
+
const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
36714
|
+
for (const property of objectExpression.properties ?? []) {
|
|
36715
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
36716
|
+
const value = skipTsExpression(property.value);
|
|
36717
|
+
if (isNodeOfType(value, "Identifier")) {
|
|
36718
|
+
if (state.localComponentNames.has(value.name)) return true;
|
|
36719
|
+
continue;
|
|
36720
|
+
}
|
|
36721
|
+
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
36722
|
+
if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
|
|
36723
|
+
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
36724
|
+
}
|
|
36725
|
+
return false;
|
|
36726
|
+
};
|
|
36618
36727
|
const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
36619
36728
|
if (initializer) {
|
|
36620
36729
|
const expression = skipTsExpression(initializer);
|
|
@@ -36645,6 +36754,10 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
|
36645
36754
|
reportNode
|
|
36646
36755
|
};
|
|
36647
36756
|
}
|
|
36757
|
+
if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
|
|
36758
|
+
kind: "namespace-object",
|
|
36759
|
+
reportNode
|
|
36760
|
+
};
|
|
36648
36761
|
if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
|
|
36649
36762
|
kind: "non-component",
|
|
36650
36763
|
reportNode
|
|
@@ -36661,7 +36774,7 @@ const collectRelevantNodes = (programRoot) => {
|
|
|
36661
36774
|
walkAst(programRoot, (child) => {
|
|
36662
36775
|
const childType = child.type;
|
|
36663
36776
|
if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
|
|
36664
|
-
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
|
|
36777
|
+
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
|
|
36665
36778
|
});
|
|
36666
36779
|
return {
|
|
36667
36780
|
exportNodes,
|
|
@@ -36706,18 +36819,31 @@ const onlyExportComponents = defineRule({
|
|
|
36706
36819
|
category: "Architecture",
|
|
36707
36820
|
create: (context) => {
|
|
36708
36821
|
const settings = resolveSettings$6(context.settings);
|
|
36709
|
-
const state = {
|
|
36710
|
-
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
36711
|
-
allowExportNames: new Set(settings.allowExportNames),
|
|
36712
|
-
allowConstantExport: settings.allowConstantExport
|
|
36713
|
-
};
|
|
36714
36822
|
return { Program(node) {
|
|
36715
36823
|
if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
|
|
36716
36824
|
const { exportNodes, componentCandidates } = collectRelevantNodes(node);
|
|
36825
|
+
const localComponentNames = /* @__PURE__ */ new Set();
|
|
36826
|
+
const state = {
|
|
36827
|
+
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
36828
|
+
allowExportNames: new Set(settings.allowExportNames),
|
|
36829
|
+
allowConstantExport: settings.allowConstantExport,
|
|
36830
|
+
localComponentNames
|
|
36831
|
+
};
|
|
36832
|
+
for (const child of componentCandidates) {
|
|
36833
|
+
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
36834
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
36835
|
+
}
|
|
36836
|
+
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
36837
|
+
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
36838
|
+
}
|
|
36839
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
36840
|
+
const initializer = child.init;
|
|
36841
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
36842
|
+
}
|
|
36843
|
+
}
|
|
36717
36844
|
const exports = [];
|
|
36718
36845
|
let hasReactExport = false;
|
|
36719
36846
|
let hasAnyExports = false;
|
|
36720
|
-
const localComponents = [];
|
|
36721
36847
|
const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
|
|
36722
36848
|
for (const child of exportNodes) {
|
|
36723
36849
|
if (isNodeOfType(child, "ExportAllDeclaration")) {
|
|
@@ -36788,7 +36914,14 @@ const onlyExportComponents = defineRule({
|
|
|
36788
36914
|
});
|
|
36789
36915
|
continue;
|
|
36790
36916
|
}
|
|
36791
|
-
if (isNodeOfType(stripped, "
|
|
36917
|
+
if (isNodeOfType(stripped, "ObjectExpression")) {
|
|
36918
|
+
context.report({
|
|
36919
|
+
node: stripped,
|
|
36920
|
+
message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
|
|
36921
|
+
});
|
|
36922
|
+
continue;
|
|
36923
|
+
}
|
|
36924
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
|
|
36792
36925
|
context.report({
|
|
36793
36926
|
node: stripped,
|
|
36794
36927
|
message: ANONYMOUS_MESSAGE
|
|
@@ -36841,32 +36974,23 @@ const onlyExportComponents = defineRule({
|
|
|
36841
36974
|
let entry;
|
|
36842
36975
|
if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
|
|
36843
36976
|
else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
|
|
36844
|
-
else
|
|
36845
|
-
|
|
36846
|
-
|
|
36847
|
-
|
|
36977
|
+
else {
|
|
36978
|
+
entry = {
|
|
36979
|
+
kind: "non-component",
|
|
36980
|
+
reportNode
|
|
36981
|
+
};
|
|
36982
|
+
if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
|
|
36983
|
+
}
|
|
36848
36984
|
if (isReExportFromSource && entry.kind !== "react-component") continue;
|
|
36849
36985
|
exports.push(entry);
|
|
36850
36986
|
}
|
|
36851
36987
|
}
|
|
36852
36988
|
}
|
|
36853
36989
|
for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
|
|
36854
|
-
const
|
|
36855
|
-
|
|
36856
|
-
|
|
36857
|
-
|
|
36858
|
-
walker = walker.parent ?? null;
|
|
36859
|
-
}
|
|
36860
|
-
return false;
|
|
36861
|
-
};
|
|
36862
|
-
for (const child of componentCandidates) {
|
|
36863
|
-
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
36864
|
-
if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
36865
|
-
}
|
|
36866
|
-
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
36867
|
-
if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
36868
|
-
}
|
|
36869
|
-
}
|
|
36990
|
+
for (const entry of exports) if (entry.kind === "namespace-object") context.report({
|
|
36991
|
+
node: entry.reportNode,
|
|
36992
|
+
message: NAMESPACE_OBJECT_MESSAGE
|
|
36993
|
+
});
|
|
36870
36994
|
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
36871
36995
|
if (entry.kind === "non-component") context.report({
|
|
36872
36996
|
node: entry.reportNode,
|
|
@@ -36877,14 +37001,6 @@ const onlyExportComponents = defineRule({
|
|
|
36877
37001
|
message: REACT_CONTEXT_MESSAGE
|
|
36878
37002
|
});
|
|
36879
37003
|
}
|
|
36880
|
-
else if (hasAnyExports && !hasReactExport && localComponents.length > 0) for (const local of localComponents) context.report({
|
|
36881
|
-
node: local,
|
|
36882
|
-
message: LOCAL_COMPONENT_MESSAGE
|
|
36883
|
-
});
|
|
36884
|
-
else if (!hasAnyExports && localComponents.length > 0) for (const local of localComponents) context.report({
|
|
36885
|
-
node: local,
|
|
36886
|
-
message: NO_EXPORT_MESSAGE
|
|
36887
|
-
});
|
|
36888
37004
|
} };
|
|
36889
37005
|
}
|
|
36890
37006
|
});
|
|
@@ -37682,8 +37798,8 @@ const preferHtmlDialog = defineRule({
|
|
|
37682
37798
|
if (!HTML_TAGS.has(tagName)) return;
|
|
37683
37799
|
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
37684
37800
|
if (roleAttribute) {
|
|
37685
|
-
const
|
|
37686
|
-
if (
|
|
37801
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
37802
|
+
if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
|
|
37687
37803
|
if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
|
|
37688
37804
|
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
37689
37805
|
const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
|
|
@@ -37962,7 +38078,7 @@ const preferModuleScopeStaticValue = defineRule({
|
|
|
37962
38078
|
tags: ["test-noise"],
|
|
37963
38079
|
severity: "warn",
|
|
37964
38080
|
category: "Architecture",
|
|
37965
|
-
|
|
38081
|
+
disabledWhen: ["react-compiler"],
|
|
37966
38082
|
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.",
|
|
37967
38083
|
create: (context) => ({ VariableDeclarator(node) {
|
|
37968
38084
|
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
@@ -38032,7 +38148,7 @@ const preferStableEmptyFallback = defineRule({
|
|
|
38032
38148
|
tags: ["react-jsx-only", "test-noise"],
|
|
38033
38149
|
severity: "warn",
|
|
38034
38150
|
category: "Performance",
|
|
38035
|
-
|
|
38151
|
+
disabledWhen: ["react-compiler"],
|
|
38036
38152
|
recommendation: "Make a `const EMPTY = []` (or `{}`) at module scope and use it as the `||` / `??` fallback, so the value stays the same each render.",
|
|
38037
38153
|
create: (context) => {
|
|
38038
38154
|
let memoRegistry = null;
|
|
@@ -39361,7 +39477,7 @@ const reduxUseselectorInlineDerivation = defineRule({
|
|
|
39361
39477
|
title: "useSelector derives data inline",
|
|
39362
39478
|
severity: "warn",
|
|
39363
39479
|
category: "Performance",
|
|
39364
|
-
|
|
39480
|
+
disabledWhen: ["react-compiler"],
|
|
39365
39481
|
recommendation: "Select the raw slice and memoize derivation so Redux actions do not rebuild a collection and redraw this component.",
|
|
39366
39482
|
create: (context) => {
|
|
39367
39483
|
let aliases = /* @__PURE__ */ new Set();
|
|
@@ -39421,7 +39537,7 @@ const reduxUseselectorReturnsNewCollection = defineRule({
|
|
|
39421
39537
|
title: "useSelector returns a new collection",
|
|
39422
39538
|
severity: "warn",
|
|
39423
39539
|
category: "Performance",
|
|
39424
|
-
|
|
39540
|
+
disabledWhen: ["react-compiler"],
|
|
39425
39541
|
recommendation: "Return a stable selected value, split selectors, or pass `shallowEqual` so every Redux action does not redraw this component.",
|
|
39426
39542
|
create: (context) => {
|
|
39427
39543
|
let aliases = /* @__PURE__ */ new Set();
|
|
@@ -39721,7 +39837,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
39721
39837
|
title: "Time or random value in JSX",
|
|
39722
39838
|
severity: "warn",
|
|
39723
39839
|
category: "Correctness",
|
|
39724
|
-
|
|
39840
|
+
disabledWhen: ["vite", "cra"],
|
|
39725
39841
|
recommendation: "Move time or random values into useEffect+useState so they only run in the browser, or add suppressHydrationWarning to the parent if it's intentional",
|
|
39726
39842
|
create: (context) => {
|
|
39727
39843
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
@@ -40672,6 +40788,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40672
40788
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40673
40789
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40674
40790
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40791
|
+
if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
|
|
40675
40792
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
40676
40793
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
40677
40794
|
context.report({
|
|
@@ -40704,18 +40821,6 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
40704
40821
|
"getUTCMilliseconds",
|
|
40705
40822
|
"valueOf"
|
|
40706
40823
|
]);
|
|
40707
|
-
const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
|
|
40708
|
-
"Date",
|
|
40709
|
-
"Map",
|
|
40710
|
-
"Set",
|
|
40711
|
-
"WeakMap",
|
|
40712
|
-
"WeakSet",
|
|
40713
|
-
"RegExp",
|
|
40714
|
-
"Error",
|
|
40715
|
-
"URL",
|
|
40716
|
-
"URLSearchParams",
|
|
40717
|
-
"AbortController"
|
|
40718
|
-
]);
|
|
40719
40824
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
40720
40825
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
40721
40826
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
@@ -41459,7 +41564,7 @@ const rnBottomSheetPreferNative = defineRule({
|
|
|
41459
41564
|
});
|
|
41460
41565
|
//#endregion
|
|
41461
41566
|
//#region src/plugin/rules/react-native/rn-detox-missing-await.ts
|
|
41462
|
-
const EMPTY_VISITORS$
|
|
41567
|
+
const EMPTY_VISITORS$5 = {};
|
|
41463
41568
|
const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
|
|
41464
41569
|
const DETOX_ELEMENT_ACTIONS = new Set([
|
|
41465
41570
|
"tap",
|
|
@@ -41519,7 +41624,7 @@ const rnDetoxMissingAwait = defineRule({
|
|
|
41519
41624
|
recommendation: "Prepend `await` to Detox actions, `waitFor(...)` chains, and `expect(element(...))` assertions. They return promises tied to Detox's synchronization, so a missing await runs steps out of order.",
|
|
41520
41625
|
create: (context) => {
|
|
41521
41626
|
const filename = normalizeFilename(context.filename ?? "");
|
|
41522
|
-
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$
|
|
41627
|
+
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
|
|
41523
41628
|
return { ExpressionStatement(node) {
|
|
41524
41629
|
const expression = node.expression;
|
|
41525
41630
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
@@ -41688,7 +41793,7 @@ const rnListCallbackPerRow = defineRule({
|
|
|
41688
41793
|
tags: ["test-noise"],
|
|
41689
41794
|
requires: ["react-native"],
|
|
41690
41795
|
severity: "warn",
|
|
41691
|
-
|
|
41796
|
+
disabledWhen: ["react-compiler"],
|
|
41692
41797
|
recommendation: "Move the handler out with useCallback at list scope and pass the row id as a prop. Then memo() rows skip redrawing when their data has not changed.",
|
|
41693
41798
|
create: (context) => {
|
|
41694
41799
|
const inspect = (node) => {
|
|
@@ -42316,7 +42421,7 @@ const rnNoInlineFlatlistRenderitem = defineRule({
|
|
|
42316
42421
|
tags: ["test-noise"],
|
|
42317
42422
|
requires: ["react-native"],
|
|
42318
42423
|
severity: "warn",
|
|
42319
|
-
|
|
42424
|
+
disabledWhen: ["react-compiler"],
|
|
42320
42425
|
recommendation: "Move renderItem to a named function or wrap it in useCallback so it is not rebuilt every time the screen redraws.",
|
|
42321
42426
|
create: (context) => ({ JSXAttribute(node) {
|
|
42322
42427
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "renderItem") return;
|
|
@@ -42341,7 +42446,7 @@ const rnNoInlineObjectInListItem = defineRule({
|
|
|
42341
42446
|
tags: ["test-noise"],
|
|
42342
42447
|
requires: ["react-native"],
|
|
42343
42448
|
severity: "warn",
|
|
42344
|
-
|
|
42449
|
+
disabledWhen: ["react-compiler"],
|
|
42345
42450
|
recommendation: "Move style and object props out of renderItem (StyleSheet.create, useMemo at list scope, or pass primitives) so memo() rows stop redrawing when their data has not changed.",
|
|
42346
42451
|
create: (context) => {
|
|
42347
42452
|
const renderPropStack = [];
|
|
@@ -42494,7 +42599,7 @@ const isLegacyArchReactNativeFile = (filename) => {
|
|
|
42494
42599
|
};
|
|
42495
42600
|
//#endregion
|
|
42496
42601
|
//#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
|
|
42497
|
-
const EMPTY_VISITORS$
|
|
42602
|
+
const EMPTY_VISITORS$4 = {};
|
|
42498
42603
|
const reportLegacyShadowProperties = (objectExpression, context) => {
|
|
42499
42604
|
const legacyShadowPropertyNames = [];
|
|
42500
42605
|
for (const property of objectExpression.properties ?? []) {
|
|
@@ -42517,7 +42622,7 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42517
42622
|
severity: "warn",
|
|
42518
42623
|
recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
|
|
42519
42624
|
create: (context) => {
|
|
42520
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
42625
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
|
|
42521
42626
|
return {
|
|
42522
42627
|
JSXAttribute(node) {
|
|
42523
42628
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -43438,7 +43543,7 @@ const isExpoManagedFileActive = (context) => {
|
|
|
43438
43543
|
};
|
|
43439
43544
|
//#endregion
|
|
43440
43545
|
//#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
|
|
43441
|
-
const EMPTY_VISITORS$
|
|
43546
|
+
const EMPTY_VISITORS$3 = {};
|
|
43442
43547
|
const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
|
|
43443
43548
|
const MEMBER_PATH_FAN_OUT_LIMIT = 32;
|
|
43444
43549
|
const isRequireOfBundledAsset = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "require" && node.arguments?.length === 1 && isNodeOfType(node.arguments[0], "Literal") && typeof node.arguments[0].value === "string" && BUNDLED_ASSET_SOURCE_PATTERN.test(node.arguments[0].value);
|
|
@@ -43472,7 +43577,7 @@ const rnPreferExpoImage = defineRule({
|
|
|
43472
43577
|
severity: "warn",
|
|
43473
43578
|
recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
|
|
43474
43579
|
create: (context) => {
|
|
43475
|
-
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$
|
|
43580
|
+
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
|
|
43476
43581
|
const flaggedImports = [];
|
|
43477
43582
|
const assetBindingNames = /* @__PURE__ */ new Set();
|
|
43478
43583
|
const moduleObjectLiterals = /* @__PURE__ */ new Map();
|
|
@@ -43959,7 +44064,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
|
|
|
43959
44064
|
});
|
|
43960
44065
|
//#endregion
|
|
43961
44066
|
//#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
|
|
43962
|
-
const EMPTY_VISITORS$
|
|
44067
|
+
const EMPTY_VISITORS$2 = {};
|
|
43963
44068
|
const IOS_SHADOW_KEYS = new Set([
|
|
43964
44069
|
"shadowColor",
|
|
43965
44070
|
"shadowOffset",
|
|
@@ -44045,7 +44150,7 @@ const rnStylePreferBoxShadow = defineRule({
|
|
|
44045
44150
|
severity: "warn",
|
|
44046
44151
|
recommendation: "These shadow keys only work on one platform. On RN v7+, use the CSS `boxShadow` string instead, like `boxShadow: \"0 2px 8px rgba(0,0,0,0.1)\"`, which works on both.",
|
|
44047
44152
|
create: (context) => {
|
|
44048
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
44153
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
|
|
44049
44154
|
return {
|
|
44050
44155
|
JSXAttribute(node) {
|
|
44051
44156
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -44142,9 +44247,9 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
44142
44247
|
if (!HTML_TAGS.has(elementType)) return;
|
|
44143
44248
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
44144
44249
|
if (!roleAttribute) return;
|
|
44145
|
-
const
|
|
44146
|
-
if (
|
|
44147
|
-
const roles =
|
|
44250
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
44251
|
+
if (roleCandidates === null) return;
|
|
44252
|
+
const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
|
|
44148
44253
|
for (const role of roles) {
|
|
44149
44254
|
const required = ROLE_REQUIRED_PROPS.get(role);
|
|
44150
44255
|
if (!required) continue;
|
|
@@ -47261,7 +47366,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
47261
47366
|
};
|
|
47262
47367
|
//#endregion
|
|
47263
47368
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
47264
|
-
const buildMessageDefault = (
|
|
47369
|
+
const buildMessageDefault = (roles, propName) => {
|
|
47370
|
+
return `Screen reader users get no help from \`${propName}\` because role ${roles.map((role) => `\`${role}\``).join(" / ")} ignores it, so remove it or change the role.`;
|
|
47371
|
+
};
|
|
47265
47372
|
const buildMessageImplicit = (role, propName, elementType) => `Screen reader users get no help from \`${propName}\` because \`${elementType}\` has role \`${role}\`, which ignores it, so remove \`${propName}\` or change the element.`;
|
|
47266
47373
|
const roleSupportsAriaProps = defineRule({
|
|
47267
47374
|
id: "role-supports-aria-props",
|
|
@@ -47289,17 +47396,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47289
47396
|
if (!ariaAttributes) return;
|
|
47290
47397
|
const elementType = getElementType(node, context.settings);
|
|
47291
47398
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
47292
|
-
const
|
|
47293
|
-
if (
|
|
47294
|
-
|
|
47399
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
|
|
47400
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
47401
|
+
const supportedSets = [];
|
|
47402
|
+
for (const role of roleCandidates) {
|
|
47403
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
47404
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47405
|
+
if (!supported) return;
|
|
47406
|
+
supportedSets.push(supported);
|
|
47407
|
+
}
|
|
47295
47408
|
const isImplicit = !roleAttribute;
|
|
47296
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47297
|
-
if (!supported) return;
|
|
47298
47409
|
for (const { attribute, propName } of ariaAttributes) {
|
|
47299
|
-
if (supported.has(propName)) continue;
|
|
47410
|
+
if (supportedSets.some((supported) => supported.has(propName))) continue;
|
|
47300
47411
|
context.report({
|
|
47301
47412
|
node: attribute,
|
|
47302
|
-
message: isImplicit ? buildMessageImplicit(
|
|
47413
|
+
message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
|
|
47303
47414
|
});
|
|
47304
47415
|
}
|
|
47305
47416
|
} })
|
|
@@ -47651,11 +47762,15 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
47651
47762
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47652
47763
|
return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
|
|
47653
47764
|
};
|
|
47654
|
-
const
|
|
47655
|
-
const
|
|
47765
|
+
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
47766
|
+
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
47767
|
+
return Boolean(symbol && symbol.kind !== "import");
|
|
47768
|
+
};
|
|
47769
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
|
|
47770
|
+
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
47656
47771
|
const initializer = symbol.initializer;
|
|
47657
47772
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47658
|
-
return isNonReactEffectEventCallee(initializer.callee, contextNode);
|
|
47773
|
+
return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
|
|
47659
47774
|
};
|
|
47660
47775
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
47661
47776
|
let current = node.parent;
|
|
@@ -47734,7 +47849,7 @@ const rulesOfHooks = defineRule({
|
|
|
47734
47849
|
const hookContext = isHookCall$1(node, context.scopes, settings);
|
|
47735
47850
|
if (!hookContext) return;
|
|
47736
47851
|
const { hookName } = hookContext;
|
|
47737
|
-
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
|
|
47852
|
+
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
|
|
47738
47853
|
context.report({
|
|
47739
47854
|
node: node.callee,
|
|
47740
47855
|
message: buildEffectEventPassedDownMessage()
|
|
@@ -47847,7 +47962,7 @@ const rulesOfHooks = defineRule({
|
|
|
47847
47962
|
Identifier(node) {
|
|
47848
47963
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
47849
47964
|
if (!symbol || !isUseEffectEventSymbol(symbol)) return;
|
|
47850
|
-
if (isNonReactEffectEventSymbol(symbol, node)) return;
|
|
47965
|
+
if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
|
|
47851
47966
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
47852
47967
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
47853
47968
|
context.report({
|
|
@@ -48452,7 +48567,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
48452
48567
|
id: "server-fetch-without-revalidate",
|
|
48453
48568
|
title: "Fetch without revalidate",
|
|
48454
48569
|
severity: "warn",
|
|
48455
|
-
|
|
48570
|
+
disabledWhen: ["nextjs:15", "nextjs:static-export"],
|
|
48456
48571
|
recommendation: "Pass `{ next: { revalidate: <seconds> } }` (or `cache: \"no-store\"`) so old data doesn't stick around.",
|
|
48457
48572
|
create: (context) => {
|
|
48458
48573
|
let isServerSideFile = false;
|
|
@@ -55419,6 +55534,34 @@ const reactDoctorRules = [
|
|
|
55419
55534
|
];
|
|
55420
55535
|
const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
|
|
55421
55536
|
//#endregion
|
|
55537
|
+
//#region src/plugin/utils/is-next-file.ts
|
|
55538
|
+
const isNextFileActive = (context) => {
|
|
55539
|
+
const rawFilename = context.filename;
|
|
55540
|
+
if (!rawFilename) return true;
|
|
55541
|
+
const filename = normalizeFilename(rawFilename);
|
|
55542
|
+
const manifest = readNearestPackageManifest(filename);
|
|
55543
|
+
if (!manifest) return true;
|
|
55544
|
+
if (declaresDependency(manifest, "next")) return true;
|
|
55545
|
+
if (!declaresAnyDependency(manifest)) return true;
|
|
55546
|
+
const packageDirectory = findNearestPackageDirectory(filename);
|
|
55547
|
+
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
55548
|
+
if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
|
|
55549
|
+
return true;
|
|
55550
|
+
};
|
|
55551
|
+
//#endregion
|
|
55552
|
+
//#region src/plugin/utils/wrap-nextjs-rule.ts
|
|
55553
|
+
const EMPTY_VISITORS$1 = {};
|
|
55554
|
+
const wrapNextjsRule = (rule) => {
|
|
55555
|
+
const innerCreate = rule.create.bind(rule);
|
|
55556
|
+
return {
|
|
55557
|
+
...rule,
|
|
55558
|
+
create: (context) => {
|
|
55559
|
+
if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
|
|
55560
|
+
return innerCreate(context);
|
|
55561
|
+
}
|
|
55562
|
+
};
|
|
55563
|
+
};
|
|
55564
|
+
//#endregion
|
|
55422
55565
|
//#region src/plugin/utils/wrap-react-native-rule.ts
|
|
55423
55566
|
const EMPTY_VISITORS = {};
|
|
55424
55567
|
const wrapReactNativeRule = (rule) => {
|
|
@@ -55888,9 +56031,14 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
55888
56031
|
});
|
|
55889
56032
|
//#endregion
|
|
55890
56033
|
//#region src/plugin/react-doctor-plugin.ts
|
|
56034
|
+
const applyFrameworkGate = (rule) => {
|
|
56035
|
+
if (rule.framework === "react-native") return wrapReactNativeRule(rule);
|
|
56036
|
+
if (rule.framework === "nextjs") return wrapNextjsRule(rule);
|
|
56037
|
+
return rule;
|
|
56038
|
+
};
|
|
55891
56039
|
const applyFrameworkRuleWrappers = (registry) => {
|
|
55892
56040
|
const wrapped = {};
|
|
55893
|
-
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(
|
|
56041
|
+
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
|
|
55894
56042
|
return wrapped;
|
|
55895
56043
|
};
|
|
55896
56044
|
const plugin = {
|
|
@@ -56227,9 +56375,23 @@ const classifySecurityScanFile = (relativePath) => {
|
|
|
56227
56375
|
};
|
|
56228
56376
|
const shouldReadSecurityScanContent = (relativePath, isGeneratedBundle) => isGeneratedBundle || isProbablyTextFile(relativePath) || isConfigOrCiPath(relativePath) || isRepositorySecretFilePath(relativePath);
|
|
56229
56377
|
//#endregion
|
|
56378
|
+
//#region src/plugin/utils/capability.ts
|
|
56379
|
+
const FRAMEWORK_TOKENS = [
|
|
56380
|
+
"nextjs",
|
|
56381
|
+
"vite",
|
|
56382
|
+
"cra",
|
|
56383
|
+
"remix",
|
|
56384
|
+
"gatsby",
|
|
56385
|
+
"expo",
|
|
56386
|
+
"react-native",
|
|
56387
|
+
"tanstack-start",
|
|
56388
|
+
"preact",
|
|
56389
|
+
"unknown"
|
|
56390
|
+
];
|
|
56391
|
+
//#endregion
|
|
56230
56392
|
//#region src/index.ts
|
|
56231
56393
|
var src_default = plugin;
|
|
56232
56394
|
//#endregion
|
|
56233
|
-
export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, UNBOUNDED_CROSS_FILE_RULE_IDS, classifySecurityScanFile, collectCrossFileDependencyProbes, src_default as default, isReactNativeDependencyName, resetManifestCaches, shouldReadSecurityScanContent };
|
|
56395
|
+
export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, UNBOUNDED_CROSS_FILE_RULE_IDS, classifySecurityScanFile, collectCrossFileDependencyProbes, src_default as default, isReactNativeDependencyName, resetManifestCaches, shouldReadSecurityScanContent };
|
|
56234
56396
|
|
|
56235
56397
|
//# sourceMappingURL=index.js.map
|