oxlint-plugin-react-doctor 0.7.2-dev.43d766f → 0.7.2-dev.82e0475
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.js +559 -249
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -532,6 +532,12 @@ const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
|
|
|
532
532
|
]);
|
|
533
533
|
const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
|
|
534
534
|
const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
|
|
535
|
+
const SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP = new Set([
|
|
536
|
+
"WebSocket",
|
|
537
|
+
"EventSource",
|
|
538
|
+
"BroadcastChannel",
|
|
539
|
+
"RTCPeerConnection"
|
|
540
|
+
]);
|
|
535
541
|
const MUTABLE_GLOBAL_ROOTS = new Set([
|
|
536
542
|
"location",
|
|
537
543
|
"window",
|
|
@@ -595,6 +601,19 @@ const TRIVIAL_INITIALIZER_NAMES = new Set([
|
|
|
595
601
|
"parseInt",
|
|
596
602
|
"parseFloat"
|
|
597
603
|
]);
|
|
604
|
+
const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
|
|
605
|
+
"Date",
|
|
606
|
+
"Map",
|
|
607
|
+
"Set",
|
|
608
|
+
"WeakMap",
|
|
609
|
+
"WeakSet",
|
|
610
|
+
"WeakRef",
|
|
611
|
+
"RegExp",
|
|
612
|
+
"Error",
|
|
613
|
+
"URL",
|
|
614
|
+
"URLSearchParams",
|
|
615
|
+
"AbortController"
|
|
616
|
+
]);
|
|
598
617
|
const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
|
|
599
618
|
"Boolean",
|
|
600
619
|
"String",
|
|
@@ -674,7 +693,10 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
|
|
|
674
693
|
"unwatch",
|
|
675
694
|
"unlisten",
|
|
676
695
|
"unsub",
|
|
677
|
-
"abort"
|
|
696
|
+
"abort",
|
|
697
|
+
"disconnect",
|
|
698
|
+
"unobserve",
|
|
699
|
+
"close"
|
|
678
700
|
]);
|
|
679
701
|
const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
|
|
680
702
|
"remove",
|
|
@@ -2235,6 +2257,38 @@ const anchorHasContent = defineRule({
|
|
|
2235
2257
|
}
|
|
2236
2258
|
});
|
|
2237
2259
|
//#endregion
|
|
2260
|
+
//#region src/plugin/utils/get-jsx-prop-static-string-values.ts
|
|
2261
|
+
const MAX_CONST_RESOLUTION_HOPS = 4;
|
|
2262
|
+
const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
|
|
2263
|
+
const expression = stripParenExpression(rawExpression);
|
|
2264
|
+
if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
|
|
2265
|
+
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2266
|
+
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2267
|
+
return staticValue === null ? null : [staticValue];
|
|
2268
|
+
}
|
|
2269
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
2270
|
+
const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
|
|
2271
|
+
if (consequentValues === null) return null;
|
|
2272
|
+
const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
|
|
2273
|
+
if (alternateValues === null) return null;
|
|
2274
|
+
return [...consequentValues, ...alternateValues];
|
|
2275
|
+
}
|
|
2276
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
2277
|
+
if (remainingHops === 0) return null;
|
|
2278
|
+
const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
|
|
2279
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
|
|
2280
|
+
return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
|
|
2281
|
+
}
|
|
2282
|
+
return null;
|
|
2283
|
+
};
|
|
2284
|
+
const getJsxPropStaticStringValues = (attribute, scopes) => {
|
|
2285
|
+
const value = attribute.value;
|
|
2286
|
+
if (!value) return null;
|
|
2287
|
+
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? [value.value] : null;
|
|
2288
|
+
if (isNodeOfType(value, "JSXExpressionContainer")) return resolveStaticStringValues(value.expression, scopes, MAX_CONST_RESOLUTION_HOPS);
|
|
2289
|
+
return null;
|
|
2290
|
+
};
|
|
2291
|
+
//#endregion
|
|
2238
2292
|
//#region src/plugin/rules/a11y/anchor-is-valid.ts
|
|
2239
2293
|
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
2294
|
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 +2318,11 @@ const isInvalidHref = (value, validHrefs) => {
|
|
|
2264
2318
|
if (validHrefs.has(value)) return false;
|
|
2265
2319
|
return value === "" || value === "#" || value === "javascript:void(0)";
|
|
2266
2320
|
};
|
|
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;
|
|
2321
|
+
const isNullishOrFragmentHref = (value) => {
|
|
2278
2322
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2279
2323
|
const expression = value.expression;
|
|
2280
2324
|
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
|
-
}
|
|
2325
|
+
if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
|
|
2289
2326
|
}
|
|
2290
2327
|
if (isNodeOfType(value, "JSXFragment")) return true;
|
|
2291
2328
|
return false;
|
|
@@ -2318,9 +2355,10 @@ const anchorIsValid = defineRule({
|
|
|
2318
2355
|
});
|
|
2319
2356
|
return;
|
|
2320
2357
|
}
|
|
2321
|
-
|
|
2358
|
+
const hrefCandidates = getJsxPropStaticStringValues(hrefAttribute, context.scopes);
|
|
2359
|
+
if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
|
|
2322
2360
|
const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
|
|
2323
|
-
if (!hasOnClick &&
|
|
2361
|
+
if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
|
|
2324
2362
|
context.report({
|
|
2325
2363
|
node: node.name,
|
|
2326
2364
|
message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
|
|
@@ -3376,6 +3414,25 @@ const ariaRole = defineRule({
|
|
|
3376
3414
|
if (!roleAttribute) return;
|
|
3377
3415
|
const elementType = getElementType(node, context.settings);
|
|
3378
3416
|
if (settings.ignoreNonDOM && !HTML_TAGS.has(elementType)) return;
|
|
3417
|
+
const reportFirstInvalidCandidate = (candidates) => {
|
|
3418
|
+
for (const candidate of candidates) {
|
|
3419
|
+
if (candidate.trim().length === 0) {
|
|
3420
|
+
context.report({
|
|
3421
|
+
node: roleAttribute,
|
|
3422
|
+
message: buildBaseMessage("")
|
|
3423
|
+
});
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
const tokens = candidate.split(/\s+/).filter((token) => token.length > 0);
|
|
3427
|
+
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3428
|
+
context.report({
|
|
3429
|
+
node: roleAttribute,
|
|
3430
|
+
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3431
|
+
});
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
};
|
|
3379
3436
|
const value = roleAttribute.value;
|
|
3380
3437
|
if (!value) {
|
|
3381
3438
|
context.report({
|
|
@@ -3392,22 +3449,7 @@ const ariaRole = defineRule({
|
|
|
3392
3449
|
});
|
|
3393
3450
|
return;
|
|
3394
3451
|
}
|
|
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
|
-
}
|
|
3452
|
+
reportFirstInvalidCandidate([value.value]);
|
|
3411
3453
|
return;
|
|
3412
3454
|
}
|
|
3413
3455
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
@@ -3428,6 +3470,11 @@ const ariaRole = defineRule({
|
|
|
3428
3470
|
});
|
|
3429
3471
|
return;
|
|
3430
3472
|
}
|
|
3473
|
+
const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
3474
|
+
if (resolvedCandidates !== null) {
|
|
3475
|
+
reportFirstInvalidCandidate(resolvedCandidates);
|
|
3476
|
+
return;
|
|
3477
|
+
}
|
|
3431
3478
|
return;
|
|
3432
3479
|
}
|
|
3433
3480
|
context.report({
|
|
@@ -7610,6 +7657,95 @@ const displayName = defineRule({
|
|
|
7610
7657
|
}
|
|
7611
7658
|
});
|
|
7612
7659
|
//#endregion
|
|
7660
|
+
//#region src/plugin/utils/is-react-hook-name.ts
|
|
7661
|
+
const isReactHookName = (name) => {
|
|
7662
|
+
if (!name.startsWith("use")) return false;
|
|
7663
|
+
if (name.length === 3) return true;
|
|
7664
|
+
const fourthCharacter = name.charCodeAt(3);
|
|
7665
|
+
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
7666
|
+
};
|
|
7667
|
+
//#endregion
|
|
7668
|
+
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
7669
|
+
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
7670
|
+
//#endregion
|
|
7671
|
+
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
7672
|
+
const hocWrapperCalleeName = (callee) => {
|
|
7673
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
7674
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
7675
|
+
return null;
|
|
7676
|
+
};
|
|
7677
|
+
const displayNameFromFunctionBinding = (functionNode) => {
|
|
7678
|
+
let current = functionNode;
|
|
7679
|
+
for (;;) {
|
|
7680
|
+
const parent = current.parent;
|
|
7681
|
+
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
7682
|
+
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
7683
|
+
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
7684
|
+
current = parent;
|
|
7685
|
+
continue;
|
|
7686
|
+
}
|
|
7687
|
+
}
|
|
7688
|
+
break;
|
|
7689
|
+
}
|
|
7690
|
+
const binding = current.parent;
|
|
7691
|
+
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
7692
|
+
return null;
|
|
7693
|
+
};
|
|
7694
|
+
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
7695
|
+
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
7696
|
+
return displayNameFromFunctionBinding(functionNode);
|
|
7697
|
+
};
|
|
7698
|
+
//#endregion
|
|
7699
|
+
//#region src/plugin/utils/find-enclosing-function.ts
|
|
7700
|
+
const findEnclosingFunction = (node) => {
|
|
7701
|
+
let cursor = node.parent;
|
|
7702
|
+
while (cursor) {
|
|
7703
|
+
if (isFunctionLike$1(cursor)) return cursor;
|
|
7704
|
+
cursor = cursor.parent ?? null;
|
|
7705
|
+
}
|
|
7706
|
+
return null;
|
|
7707
|
+
};
|
|
7708
|
+
//#endregion
|
|
7709
|
+
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
7710
|
+
const enclosingComponentOrHookName = (node) => {
|
|
7711
|
+
const functionNode = findEnclosingFunction(node);
|
|
7712
|
+
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
7713
|
+
};
|
|
7714
|
+
//#endregion
|
|
7715
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
7716
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
7717
|
+
let node = callExpression;
|
|
7718
|
+
let parent = node.parent;
|
|
7719
|
+
while (parent) {
|
|
7720
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
7721
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
7722
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
7723
|
+
node = parent;
|
|
7724
|
+
parent = node.parent;
|
|
7725
|
+
continue;
|
|
7726
|
+
}
|
|
7727
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
7728
|
+
node = parent;
|
|
7729
|
+
parent = node.parent;
|
|
7730
|
+
continue;
|
|
7731
|
+
}
|
|
7732
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
7733
|
+
node = parent;
|
|
7734
|
+
parent = node.parent;
|
|
7735
|
+
continue;
|
|
7736
|
+
}
|
|
7737
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
7738
|
+
const expressions = parent.expressions ?? [];
|
|
7739
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
7740
|
+
node = parent;
|
|
7741
|
+
parent = node.parent;
|
|
7742
|
+
continue;
|
|
7743
|
+
}
|
|
7744
|
+
return false;
|
|
7745
|
+
}
|
|
7746
|
+
return false;
|
|
7747
|
+
};
|
|
7748
|
+
//#endregion
|
|
7613
7749
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
7614
7750
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
7615
7751
|
if (!node || typeof node !== "object") return;
|
|
@@ -7761,6 +7897,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
7761
7897
|
};
|
|
7762
7898
|
//#endregion
|
|
7763
7899
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
7900
|
+
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
7901
|
+
const RESOURCE_NOUN_BY_KIND = {
|
|
7902
|
+
subscribe: "subscription",
|
|
7903
|
+
timer: "timer",
|
|
7904
|
+
socket: "connection"
|
|
7905
|
+
};
|
|
7906
|
+
const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
|
|
7907
|
+
const isSubscribeOrObserveCall = (node) => {
|
|
7908
|
+
if (isSubscribeLikeCallExpression(node)) return true;
|
|
7909
|
+
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
7910
|
+
};
|
|
7764
7911
|
const findSubscribeLikeUsages = (callback) => {
|
|
7765
7912
|
const usages = [];
|
|
7766
7913
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
@@ -7772,6 +7919,14 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7772
7919
|
}
|
|
7773
7920
|
walkAst(callback, (child) => {
|
|
7774
7921
|
if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
|
|
7922
|
+
if (isSocketConstruction(child)) {
|
|
7923
|
+
usages.push({
|
|
7924
|
+
kind: "socket",
|
|
7925
|
+
node: child,
|
|
7926
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
7927
|
+
});
|
|
7928
|
+
return;
|
|
7929
|
+
}
|
|
7775
7930
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7776
7931
|
if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
|
|
7777
7932
|
usages.push({
|
|
@@ -7781,7 +7936,7 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7781
7936
|
});
|
|
7782
7937
|
return;
|
|
7783
7938
|
}
|
|
7784
|
-
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
|
|
7939
|
+
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) usages.push({
|
|
7785
7940
|
kind: "subscribe",
|
|
7786
7941
|
node: child,
|
|
7787
7942
|
resourceName: child.callee.property.name
|
|
@@ -7804,7 +7959,12 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7804
7959
|
const bindingName = declarator.id.name;
|
|
7805
7960
|
bindings.effectScopeVariableNames.add(bindingName);
|
|
7806
7961
|
const init = declarator.init;
|
|
7807
|
-
if (!init
|
|
7962
|
+
if (!init) continue;
|
|
7963
|
+
if (isSocketConstruction(init)) {
|
|
7964
|
+
bindings.subscriptionNames.add(bindingName);
|
|
7965
|
+
continue;
|
|
7966
|
+
}
|
|
7967
|
+
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
7808
7968
|
if (isSubscribeLikeCallExpression(init)) {
|
|
7809
7969
|
bindings.subscriptionNames.add(bindingName);
|
|
7810
7970
|
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
@@ -7856,26 +8016,168 @@ const effectHasCleanupReturn = (callback, usages) => {
|
|
|
7856
8016
|
});
|
|
7857
8017
|
return didFindCleanupReturn;
|
|
7858
8018
|
};
|
|
8019
|
+
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
8020
|
+
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some((property) => isNodeOfType(property, "Property") && isNodeOfType(property.key, "Identifier") && (property.key.name === "signal" || property.key.name === "once" && isNodeOfType(property.value, "Literal") && property.value.value === true)));
|
|
8021
|
+
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
8022
|
+
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
8023
|
+
["addListener", new Set([
|
|
8024
|
+
"removeListener",
|
|
8025
|
+
"off",
|
|
8026
|
+
"abort"
|
|
8027
|
+
])],
|
|
8028
|
+
["on", new Set([
|
|
8029
|
+
"off",
|
|
8030
|
+
"removeListener",
|
|
8031
|
+
"on"
|
|
8032
|
+
])],
|
|
8033
|
+
["subscribe", new Set(["unsubscribe", "unsub"])],
|
|
8034
|
+
["sub", new Set(["unsub", "unsubscribe"])],
|
|
8035
|
+
["watch", new Set(["unwatch", "close"])],
|
|
8036
|
+
["listen", new Set(["unlisten", "close"])],
|
|
8037
|
+
[OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
|
|
8038
|
+
]);
|
|
8039
|
+
const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
8040
|
+
"cleanup",
|
|
8041
|
+
"dispose",
|
|
8042
|
+
"destroy",
|
|
8043
|
+
"teardown"
|
|
8044
|
+
]);
|
|
8045
|
+
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8046
|
+
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
8047
|
+
const getReleaseVerbName = (node) => {
|
|
8048
|
+
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
8049
|
+
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8050
|
+
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8051
|
+
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
8052
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8053
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
8054
|
+
return null;
|
|
8055
|
+
};
|
|
8056
|
+
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
8057
|
+
const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
|
|
8058
|
+
let didFindPairedRelease = false;
|
|
8059
|
+
walkAst(body, (child) => {
|
|
8060
|
+
if (didFindPairedRelease) return false;
|
|
8061
|
+
if (child !== body && isFunctionLike$1(child)) return false;
|
|
8062
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8063
|
+
if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
|
|
8064
|
+
didFindPairedRelease = true;
|
|
8065
|
+
return false;
|
|
8066
|
+
}
|
|
8067
|
+
});
|
|
8068
|
+
return didFindPairedRelease;
|
|
8069
|
+
};
|
|
8070
|
+
const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
|
|
8071
|
+
const collectFileReleaseVerbNames = (anyNode) => {
|
|
8072
|
+
let programNode = anyNode;
|
|
8073
|
+
while (programNode.parent) programNode = programNode.parent;
|
|
8074
|
+
const cached = fileReleaseVerbNamesCache.get(programNode);
|
|
8075
|
+
if (cached) return cached;
|
|
8076
|
+
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
8077
|
+
walkAst(programNode, (child) => {
|
|
8078
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8079
|
+
if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
|
|
8080
|
+
});
|
|
8081
|
+
fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
|
|
8082
|
+
return releaseVerbNames;
|
|
8083
|
+
};
|
|
8084
|
+
const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
|
|
8085
|
+
const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
|
|
8086
|
+
const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
|
|
8087
|
+
if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
|
|
8088
|
+
for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
|
|
8089
|
+
return false;
|
|
8090
|
+
};
|
|
8091
|
+
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
8092
|
+
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
8093
|
+
const body = retainedFunction.body;
|
|
8094
|
+
if (!body) return null;
|
|
8095
|
+
let leak = null;
|
|
8096
|
+
walkAst(body, (child) => {
|
|
8097
|
+
if (leak !== null) return false;
|
|
8098
|
+
if (isFunctionLike$1(child)) return false;
|
|
8099
|
+
if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
|
|
8100
|
+
leak = {
|
|
8101
|
+
kind: "socket",
|
|
8102
|
+
node: child,
|
|
8103
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8104
|
+
};
|
|
8105
|
+
return false;
|
|
8106
|
+
}
|
|
8107
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8108
|
+
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
|
|
8109
|
+
leak = {
|
|
8110
|
+
kind: "timer",
|
|
8111
|
+
node: child,
|
|
8112
|
+
resourceName: "setInterval"
|
|
8113
|
+
};
|
|
8114
|
+
return false;
|
|
8115
|
+
}
|
|
8116
|
+
if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
|
|
8117
|
+
const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
|
|
8118
|
+
if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
|
|
8119
|
+
kind: "subscribe",
|
|
8120
|
+
node: child,
|
|
8121
|
+
resourceName: registrationVerbName
|
|
8122
|
+
};
|
|
8123
|
+
return false;
|
|
8124
|
+
}
|
|
8125
|
+
});
|
|
8126
|
+
return leak;
|
|
8127
|
+
};
|
|
8128
|
+
const isRetainedComponentScopeFunction = (functionNode) => {
|
|
8129
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
|
|
8130
|
+
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
8131
|
+
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
8132
|
+
return enclosingComponentOrHookName(functionNode) !== null;
|
|
8133
|
+
};
|
|
7859
8134
|
const effectNeedsCleanup = defineRule({
|
|
7860
8135
|
id: "effect-needs-cleanup",
|
|
7861
8136
|
title: "Effect subscription or timer never cleaned up",
|
|
7862
8137
|
severity: "error",
|
|
7863
8138
|
tags: ["test-noise"],
|
|
7864
|
-
recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, or `return unsubscribe` if the subscribe call already gave you one.",
|
|
7865
|
-
create: (context) =>
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
8139
|
+
recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
|
|
8140
|
+
create: (context) => {
|
|
8141
|
+
const reportRetainedLeak = (retainedFunction) => {
|
|
8142
|
+
const leak = findRetainedFunctionLeak(retainedFunction);
|
|
8143
|
+
if (!leak) return;
|
|
8144
|
+
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
8145
|
+
context.report({
|
|
8146
|
+
node: leak.node,
|
|
8147
|
+
message: `\`${leak.resourceName}\` creates a ${resourceNoun} in a function that outlives the render, with no cleanup path. Store the handle and release it, or move this into a useEffect that returns cleanup, so it does not leak after unmount.`
|
|
8148
|
+
});
|
|
8149
|
+
};
|
|
8150
|
+
return {
|
|
8151
|
+
CallExpression(node) {
|
|
8152
|
+
if (isHookCall$2(node, "useCallback")) {
|
|
8153
|
+
const retainedCallback = getEffectCallback(node);
|
|
8154
|
+
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
8155
|
+
return;
|
|
8156
|
+
}
|
|
8157
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
8158
|
+
const callback = getEffectCallback(node);
|
|
8159
|
+
if (!callback) return;
|
|
8160
|
+
const usages = findSubscribeLikeUsages(callback);
|
|
8161
|
+
if (usages.length === 0) return;
|
|
8162
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
8163
|
+
const firstUsage = usages[0];
|
|
8164
|
+
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
8165
|
+
context.report({
|
|
8166
|
+
node,
|
|
8167
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
8168
|
+
});
|
|
8169
|
+
},
|
|
8170
|
+
FunctionDeclaration(node) {
|
|
8171
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8172
|
+
},
|
|
8173
|
+
ArrowFunctionExpression(node) {
|
|
8174
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8175
|
+
},
|
|
8176
|
+
FunctionExpression(node) {
|
|
8177
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8178
|
+
}
|
|
8179
|
+
};
|
|
8180
|
+
}
|
|
7879
8181
|
});
|
|
7880
8182
|
//#endregion
|
|
7881
8183
|
//#region src/plugin/semantic/scope-analysis.ts
|
|
@@ -8434,17 +8736,6 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
8434
8736
|
return computedCaptures;
|
|
8435
8737
|
};
|
|
8436
8738
|
//#endregion
|
|
8437
|
-
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8438
|
-
const isReactHookName = (name) => {
|
|
8439
|
-
if (!name.startsWith("use")) return false;
|
|
8440
|
-
if (name.length === 3) return true;
|
|
8441
|
-
const fourthCharacter = name.charCodeAt(3);
|
|
8442
|
-
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
8443
|
-
};
|
|
8444
|
-
//#endregion
|
|
8445
|
-
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
8446
|
-
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
8447
|
-
//#endregion
|
|
8448
8739
|
//#region src/plugin/utils/is-react-hoc-callback-argument.ts
|
|
8449
8740
|
const reactHocCalleeName = (callee) => {
|
|
8450
8741
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
@@ -9616,7 +9907,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9616
9907
|
});
|
|
9617
9908
|
//#endregion
|
|
9618
9909
|
//#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
|
|
9619
|
-
const EMPTY_VISITORS$
|
|
9910
|
+
const EMPTY_VISITORS$6 = {};
|
|
9620
9911
|
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
9912
|
const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
|
|
9622
9913
|
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 +9919,7 @@ const expoNoNonInlinedEnv = defineRule({
|
|
|
9628
9919
|
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
9920
|
create: (context) => {
|
|
9630
9921
|
const filename = normalizeFilename(context.filename ?? "");
|
|
9631
|
-
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$
|
|
9922
|
+
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$6;
|
|
9632
9923
|
return {
|
|
9633
9924
|
MemberExpression(node) {
|
|
9634
9925
|
if (!node.computed) return;
|
|
@@ -11046,8 +11337,8 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11046
11337
|
if (node.attributes.length === 0) return;
|
|
11047
11338
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
11048
11339
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
11049
|
-
const
|
|
11050
|
-
if (
|
|
11340
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
|
|
11341
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
11051
11342
|
let hasInteractiveHandler = false;
|
|
11052
11343
|
for (const attribute of node.attributes) {
|
|
11053
11344
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11061,11 +11352,16 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11061
11352
|
const elementType = getElementType(node, context.settings);
|
|
11062
11353
|
if (!HTML_TAGS.has(elementType)) return;
|
|
11063
11354
|
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
11355
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
11067
|
-
|
|
11068
|
-
const
|
|
11356
|
+
const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
|
|
11357
|
+
for (const role of roleCandidates) {
|
|
11358
|
+
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11359
|
+
if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
|
|
11360
|
+
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
11361
|
+
}
|
|
11362
|
+
const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
|
|
11363
|
+
const roleDisplay = roleCandidates.join("' / '");
|
|
11364
|
+
const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
|
|
11069
11365
|
context.report({
|
|
11070
11366
|
node,
|
|
11071
11367
|
message
|
|
@@ -11254,16 +11550,6 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
11254
11550
|
return names;
|
|
11255
11551
|
};
|
|
11256
11552
|
//#endregion
|
|
11257
|
-
//#region src/plugin/utils/find-enclosing-function.ts
|
|
11258
|
-
const findEnclosingFunction = (node) => {
|
|
11259
|
-
let cursor = node.parent;
|
|
11260
|
-
while (cursor) {
|
|
11261
|
-
if (isFunctionLike$1(cursor)) return cursor;
|
|
11262
|
-
cursor = cursor.parent ?? null;
|
|
11263
|
-
}
|
|
11264
|
-
return null;
|
|
11265
|
-
};
|
|
11266
|
-
//#endregion
|
|
11267
11553
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
11268
11554
|
const getFunctionBindingIdentifier = (functionNode) => {
|
|
11269
11555
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -21068,7 +21354,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
|
21068
21354
|
const noAdjustStateOnPropChange = defineRule({
|
|
21069
21355
|
id: "no-adjust-state-on-prop-change",
|
|
21070
21356
|
title: "State synced to a prop inside an effect",
|
|
21071
|
-
severity: "
|
|
21357
|
+
severity: "warn",
|
|
21072
21358
|
tags: ["test-noise"],
|
|
21073
21359
|
recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
21074
21360
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -22477,6 +22763,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
|
|
|
22477
22763
|
const section = manifest[sectionName];
|
|
22478
22764
|
return typeof section === "object" && section !== null && Object.keys(section).length > 0;
|
|
22479
22765
|
});
|
|
22766
|
+
const declaresDependency = (manifest, dependencyName) => {
|
|
22767
|
+
for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
|
|
22768
|
+
return false;
|
|
22769
|
+
};
|
|
22480
22770
|
const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
|
|
22481
22771
|
const classifyPackagePlatform = (filename) => {
|
|
22482
22772
|
const manifest = readNearestPackageManifest(filename);
|
|
@@ -22493,9 +22783,7 @@ const classifyPackagePlatform = (filename) => {
|
|
|
22493
22783
|
return result;
|
|
22494
22784
|
};
|
|
22495
22785
|
//#endregion
|
|
22496
|
-
//#region src/plugin/utils/is-
|
|
22497
|
-
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22498
|
-
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22786
|
+
//#region src/plugin/utils/is-package-nested-below-project-root.ts
|
|
22499
22787
|
const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
|
|
22500
22788
|
const resolveRealDirectory = (directory) => {
|
|
22501
22789
|
const cached = cachedRealDirectoryByDirectory.get(directory);
|
|
@@ -22516,6 +22804,10 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
|
|
|
22516
22804
|
const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
|
|
22517
22805
|
return realPackageDirectory.startsWith(rootPrefix);
|
|
22518
22806
|
};
|
|
22807
|
+
//#endregion
|
|
22808
|
+
//#region src/plugin/utils/is-react-native-file.ts
|
|
22809
|
+
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22810
|
+
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22519
22811
|
const classifyReactNativeFileTarget = (context) => {
|
|
22520
22812
|
const rawFilename = context.filename;
|
|
22521
22813
|
if (!rawFilename) return "unknown";
|
|
@@ -22932,7 +23224,6 @@ const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
|
22932
23224
|
let fallThroughCount = 0;
|
|
22933
23225
|
let maxTerminatingPathCount = 0;
|
|
22934
23226
|
for (const statement of statements) {
|
|
22935
|
-
if (isFunctionLike$1(statement)) continue;
|
|
22936
23227
|
const guardBranch = isGuardWithTerminatingBranch(statement);
|
|
22937
23228
|
if (guardBranch) {
|
|
22938
23229
|
maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
|
|
@@ -22968,14 +23259,23 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
|
|
|
22968
23259
|
if (!isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
22969
23260
|
return parent.parent === effectCallback.body;
|
|
22970
23261
|
};
|
|
23262
|
+
const countFunctionBodySetStateCalls = (functionNode, context) => {
|
|
23263
|
+
if (isAsyncFunctionLike(functionNode)) return 0;
|
|
23264
|
+
const body = functionNode.body;
|
|
23265
|
+
if (!body) return 0;
|
|
23266
|
+
return countMaxPathSetStateCalls(body, context);
|
|
23267
|
+
};
|
|
22971
23268
|
const countMaxPathSetStateCalls = (node, context) => {
|
|
22972
23269
|
if (!node || typeof node !== "object") return 0;
|
|
22973
|
-
if (
|
|
23270
|
+
if (isFunctionLike$1(node)) return 0;
|
|
22974
23271
|
if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
|
|
22975
23272
|
if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
|
|
22976
23273
|
const consequent = node.consequent;
|
|
22977
23274
|
const alternate = node.alternate;
|
|
22978
|
-
|
|
23275
|
+
const testCount = countMaxPathSetStateCalls(node.test, context);
|
|
23276
|
+
const thenCount = countMaxPathSetStateCalls(consequent, context);
|
|
23277
|
+
const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
|
|
23278
|
+
return testCount + Math.max(thenCount, elseCount);
|
|
22979
23279
|
}
|
|
22980
23280
|
if (isNodeOfType(node, "SwitchStatement")) {
|
|
22981
23281
|
let maxRunSetters = 0;
|
|
@@ -23005,28 +23305,31 @@ const countMaxPathSetStateCalls = (node, context) => {
|
|
|
23005
23305
|
}
|
|
23006
23306
|
if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
|
|
23007
23307
|
let nestedSettersInArgs = 0;
|
|
23008
|
-
for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
|
|
23308
|
+
for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$1(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
|
|
23009
23309
|
return 1 + nestedSettersInArgs;
|
|
23010
23310
|
}
|
|
23011
23311
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
|
|
23012
23312
|
const helperFunction = context.helpersByName.get(node.callee.name);
|
|
23013
23313
|
if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
|
|
23014
23314
|
context.activeHelpers.add(helperFunction);
|
|
23015
|
-
let helperCount =
|
|
23315
|
+
let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
|
|
23016
23316
|
context.activeHelpers.delete(helperFunction);
|
|
23017
23317
|
for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
|
|
23018
23318
|
return helperCount;
|
|
23019
23319
|
}
|
|
23020
23320
|
}
|
|
23021
|
-
const
|
|
23321
|
+
const countChild = (child) => {
|
|
23322
|
+
if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
|
|
23323
|
+
return countMaxPathSetStateCalls(child, context);
|
|
23324
|
+
};
|
|
23022
23325
|
let total = 0;
|
|
23023
23326
|
const nodeRecord = node;
|
|
23024
23327
|
for (const key of Object.keys(nodeRecord)) {
|
|
23025
23328
|
if (key === "parent") continue;
|
|
23026
23329
|
const child = nodeRecord[key];
|
|
23027
23330
|
if (Array.isArray(child)) {
|
|
23028
|
-
for (const item of child) if (item && typeof item === "object" && "type" in item
|
|
23029
|
-
} else if (child && typeof child === "object" && "type" in child
|
|
23331
|
+
for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
|
|
23332
|
+
} else if (child && typeof child === "object" && "type" in child) total += countChild(child);
|
|
23030
23333
|
}
|
|
23031
23334
|
return total;
|
|
23032
23335
|
};
|
|
@@ -23075,7 +23378,7 @@ const noCascadingSetState = defineRule({
|
|
|
23075
23378
|
const callback = getEffectCallback(node);
|
|
23076
23379
|
if (!callback) return;
|
|
23077
23380
|
if (isDevOnlyGuardedEffect(callback)) return;
|
|
23078
|
-
const setStateCallCount =
|
|
23381
|
+
const setStateCallCount = countFunctionBodySetStateCalls(callback, {
|
|
23079
23382
|
helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
|
|
23080
23383
|
activeHelpers: /* @__PURE__ */ new Set(),
|
|
23081
23384
|
effectCallback: callback
|
|
@@ -23419,40 +23722,6 @@ const noCloneElement = defineRule({
|
|
|
23419
23722
|
} })
|
|
23420
23723
|
});
|
|
23421
23724
|
//#endregion
|
|
23422
|
-
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
23423
|
-
const hocWrapperCalleeName = (callee) => {
|
|
23424
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
23425
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
23426
|
-
return null;
|
|
23427
|
-
};
|
|
23428
|
-
const displayNameFromFunctionBinding = (functionNode) => {
|
|
23429
|
-
let current = functionNode;
|
|
23430
|
-
for (;;) {
|
|
23431
|
-
const parent = current.parent;
|
|
23432
|
-
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
23433
|
-
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
23434
|
-
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
23435
|
-
current = parent;
|
|
23436
|
-
continue;
|
|
23437
|
-
}
|
|
23438
|
-
}
|
|
23439
|
-
break;
|
|
23440
|
-
}
|
|
23441
|
-
const binding = current.parent;
|
|
23442
|
-
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
23443
|
-
return null;
|
|
23444
|
-
};
|
|
23445
|
-
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
23446
|
-
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
23447
|
-
return displayNameFromFunctionBinding(functionNode);
|
|
23448
|
-
};
|
|
23449
|
-
//#endregion
|
|
23450
|
-
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
23451
|
-
const enclosingComponentOrHookName = (node) => {
|
|
23452
|
-
const functionNode = findEnclosingFunction(node);
|
|
23453
|
-
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
23454
|
-
};
|
|
23455
|
-
//#endregion
|
|
23456
23725
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
23457
23726
|
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
23458
23727
|
const CONTEXT_MODULES = [
|
|
@@ -24466,6 +24735,26 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24466
24735
|
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
24467
24736
|
}
|
|
24468
24737
|
};
|
|
24738
|
+
const flattenGuardedStatements = (statements) => {
|
|
24739
|
+
const flattened = [];
|
|
24740
|
+
for (const statement of statements) {
|
|
24741
|
+
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
24742
|
+
flattened.push(statement);
|
|
24743
|
+
continue;
|
|
24744
|
+
}
|
|
24745
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
24746
|
+
for (const branch of [statement.consequent, statement.alternate]) {
|
|
24747
|
+
if (!branch) continue;
|
|
24748
|
+
const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
|
|
24749
|
+
if (flattenedBranch === null) return null;
|
|
24750
|
+
flattened.push(...flattenedBranch);
|
|
24751
|
+
}
|
|
24752
|
+
continue;
|
|
24753
|
+
}
|
|
24754
|
+
return null;
|
|
24755
|
+
}
|
|
24756
|
+
return flattened;
|
|
24757
|
+
};
|
|
24469
24758
|
const noDerivedStateEffect = defineRule({
|
|
24470
24759
|
id: "no-derived-state-effect",
|
|
24471
24760
|
title: "Derived state stored in an effect",
|
|
@@ -24497,8 +24786,8 @@ const noDerivedStateEffect = defineRule({
|
|
|
24497
24786
|
}
|
|
24498
24787
|
}
|
|
24499
24788
|
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
24500
|
-
const statements = getCallbackStatements(callback);
|
|
24501
|
-
if (statements.length === 0) return;
|
|
24789
|
+
const statements = flattenGuardedStatements(getCallbackStatements(callback));
|
|
24790
|
+
if (statements === null || statements.length === 0) return;
|
|
24502
24791
|
if (!statements.every((statement) => {
|
|
24503
24792
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24504
24793
|
const expression = statement.expression;
|
|
@@ -26293,7 +26582,11 @@ const noEffectEventInDeps = defineRule({
|
|
|
26293
26582
|
const initializer = declaratorNode.init;
|
|
26294
26583
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
26295
26584
|
if (!isHookCall$2(initializer, "useEffectEvent")) return;
|
|
26296
|
-
if (isNodeOfType(initializer.callee, "Identifier")
|
|
26585
|
+
if (isNodeOfType(initializer.callee, "Identifier")) {
|
|
26586
|
+
if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
|
|
26587
|
+
const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
|
|
26588
|
+
if (calleeSymbol && calleeSymbol.kind !== "import") return;
|
|
26589
|
+
}
|
|
26297
26590
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26298
26591
|
} });
|
|
26299
26592
|
return {
|
|
@@ -28973,7 +29266,7 @@ const extractReturnTypeAnnotation = (returnType) => {
|
|
|
28973
29266
|
const noJsxElementType = defineRule({
|
|
28974
29267
|
id: "no-jsx-element-type",
|
|
28975
29268
|
title: "No JSX.Element",
|
|
28976
|
-
severity: "
|
|
29269
|
+
severity: "warn",
|
|
28977
29270
|
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
28978
29271
|
create: (context) => {
|
|
28979
29272
|
let isJsxImported = false;
|
|
@@ -30112,7 +30405,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
30112
30405
|
"reverse",
|
|
30113
30406
|
"sort"
|
|
30114
30407
|
]);
|
|
30115
|
-
const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
|
|
30116
30408
|
const OBJECT_MUTATION_METHODS = new Set([
|
|
30117
30409
|
"assign",
|
|
30118
30410
|
"defineProperties",
|
|
@@ -30186,7 +30478,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
30186
30478
|
const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
|
|
30187
30479
|
if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
|
|
30188
30480
|
if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30189
|
-
if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30190
30481
|
}
|
|
30191
30482
|
}
|
|
30192
30483
|
if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
|
|
@@ -30225,6 +30516,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
30225
30516
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
30226
30517
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
30227
30518
|
if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
|
|
30519
|
+
if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
|
|
30228
30520
|
if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
|
|
30229
30521
|
});
|
|
30230
30522
|
return mutations;
|
|
@@ -30457,7 +30749,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
30457
30749
|
id: "no-nested-component-definition",
|
|
30458
30750
|
title: "Component defined inside another component",
|
|
30459
30751
|
tags: ["test-noise", "react-jsx-only"],
|
|
30460
|
-
severity: "
|
|
30752
|
+
severity: "warn",
|
|
30461
30753
|
category: "Correctness",
|
|
30462
30754
|
recommendation: "Move it to module scope or a separate file so React does not recreate the component and erase its state on every parent render.",
|
|
30463
30755
|
create: (context) => {
|
|
@@ -31936,40 +32228,6 @@ const noPreventDefault = defineRule({
|
|
|
31936
32228
|
}
|
|
31937
32229
|
});
|
|
31938
32230
|
//#endregion
|
|
31939
|
-
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
31940
|
-
const isResultDiscardedCall = (callExpression) => {
|
|
31941
|
-
let node = callExpression;
|
|
31942
|
-
let parent = node.parent;
|
|
31943
|
-
while (parent) {
|
|
31944
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
31945
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
31946
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
31947
|
-
node = parent;
|
|
31948
|
-
parent = node.parent;
|
|
31949
|
-
continue;
|
|
31950
|
-
}
|
|
31951
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
31952
|
-
node = parent;
|
|
31953
|
-
parent = node.parent;
|
|
31954
|
-
continue;
|
|
31955
|
-
}
|
|
31956
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
31957
|
-
node = parent;
|
|
31958
|
-
parent = node.parent;
|
|
31959
|
-
continue;
|
|
31960
|
-
}
|
|
31961
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
31962
|
-
const expressions = parent.expressions ?? [];
|
|
31963
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
31964
|
-
node = parent;
|
|
31965
|
-
parent = node.parent;
|
|
31966
|
-
continue;
|
|
31967
|
-
}
|
|
31968
|
-
return false;
|
|
31969
|
-
}
|
|
31970
|
-
return false;
|
|
31971
|
-
};
|
|
31972
|
-
//#endregion
|
|
31973
32231
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
|
|
31974
32232
|
const isRefLatchGuardedEffect = (callbackBody) => {
|
|
31975
32233
|
const refNamesReadInGuards = /* @__PURE__ */ new Set();
|
|
@@ -36575,8 +36833,7 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
|
|
|
36575
36833
|
const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
|
|
36576
36834
|
const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
|
|
36577
36835
|
const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
|
|
36578
|
-
const
|
|
36579
|
-
const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
|
|
36836
|
+
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.";
|
|
36580
36837
|
const DEFAULT_REACT_HOCS = [
|
|
36581
36838
|
"memo",
|
|
36582
36839
|
"forwardRef",
|
|
@@ -36647,6 +36904,20 @@ const isReactComponentInitializer = (expression, state) => {
|
|
|
36647
36904
|
if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
|
|
36648
36905
|
return false;
|
|
36649
36906
|
};
|
|
36907
|
+
const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
36908
|
+
for (const property of objectExpression.properties ?? []) {
|
|
36909
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
36910
|
+
const value = skipTsExpression(property.value);
|
|
36911
|
+
if (isNodeOfType(value, "Identifier")) {
|
|
36912
|
+
if (state.localComponentNames.has(value.name)) return true;
|
|
36913
|
+
continue;
|
|
36914
|
+
}
|
|
36915
|
+
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
36916
|
+
if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
|
|
36917
|
+
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
36918
|
+
}
|
|
36919
|
+
return false;
|
|
36920
|
+
};
|
|
36650
36921
|
const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
36651
36922
|
if (initializer) {
|
|
36652
36923
|
const expression = skipTsExpression(initializer);
|
|
@@ -36677,6 +36948,10 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
|
36677
36948
|
reportNode
|
|
36678
36949
|
};
|
|
36679
36950
|
}
|
|
36951
|
+
if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
|
|
36952
|
+
kind: "namespace-object",
|
|
36953
|
+
reportNode
|
|
36954
|
+
};
|
|
36680
36955
|
if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
|
|
36681
36956
|
kind: "non-component",
|
|
36682
36957
|
reportNode
|
|
@@ -36693,7 +36968,7 @@ const collectRelevantNodes = (programRoot) => {
|
|
|
36693
36968
|
walkAst(programRoot, (child) => {
|
|
36694
36969
|
const childType = child.type;
|
|
36695
36970
|
if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
|
|
36696
|
-
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
|
|
36971
|
+
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
|
|
36697
36972
|
});
|
|
36698
36973
|
return {
|
|
36699
36974
|
exportNodes,
|
|
@@ -36738,18 +37013,31 @@ const onlyExportComponents = defineRule({
|
|
|
36738
37013
|
category: "Architecture",
|
|
36739
37014
|
create: (context) => {
|
|
36740
37015
|
const settings = resolveSettings$6(context.settings);
|
|
36741
|
-
const state = {
|
|
36742
|
-
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
36743
|
-
allowExportNames: new Set(settings.allowExportNames),
|
|
36744
|
-
allowConstantExport: settings.allowConstantExport
|
|
36745
|
-
};
|
|
36746
37016
|
return { Program(node) {
|
|
36747
37017
|
if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
|
|
36748
37018
|
const { exportNodes, componentCandidates } = collectRelevantNodes(node);
|
|
37019
|
+
const localComponentNames = /* @__PURE__ */ new Set();
|
|
37020
|
+
const state = {
|
|
37021
|
+
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
37022
|
+
allowExportNames: new Set(settings.allowExportNames),
|
|
37023
|
+
allowConstantExport: settings.allowConstantExport,
|
|
37024
|
+
localComponentNames
|
|
37025
|
+
};
|
|
37026
|
+
for (const child of componentCandidates) {
|
|
37027
|
+
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
37028
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37029
|
+
}
|
|
37030
|
+
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
37031
|
+
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37032
|
+
}
|
|
37033
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
37034
|
+
const initializer = child.init;
|
|
37035
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37036
|
+
}
|
|
37037
|
+
}
|
|
36749
37038
|
const exports = [];
|
|
36750
37039
|
let hasReactExport = false;
|
|
36751
37040
|
let hasAnyExports = false;
|
|
36752
|
-
const localComponents = [];
|
|
36753
37041
|
const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
|
|
36754
37042
|
for (const child of exportNodes) {
|
|
36755
37043
|
if (isNodeOfType(child, "ExportAllDeclaration")) {
|
|
@@ -36820,7 +37108,14 @@ const onlyExportComponents = defineRule({
|
|
|
36820
37108
|
});
|
|
36821
37109
|
continue;
|
|
36822
37110
|
}
|
|
36823
|
-
if (isNodeOfType(stripped, "
|
|
37111
|
+
if (isNodeOfType(stripped, "ObjectExpression")) {
|
|
37112
|
+
context.report({
|
|
37113
|
+
node: stripped,
|
|
37114
|
+
message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
|
|
37115
|
+
});
|
|
37116
|
+
continue;
|
|
37117
|
+
}
|
|
37118
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
|
|
36824
37119
|
context.report({
|
|
36825
37120
|
node: stripped,
|
|
36826
37121
|
message: ANONYMOUS_MESSAGE
|
|
@@ -36873,32 +37168,23 @@ const onlyExportComponents = defineRule({
|
|
|
36873
37168
|
let entry;
|
|
36874
37169
|
if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
|
|
36875
37170
|
else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
|
|
36876
|
-
else
|
|
36877
|
-
|
|
36878
|
-
|
|
36879
|
-
|
|
37171
|
+
else {
|
|
37172
|
+
entry = {
|
|
37173
|
+
kind: "non-component",
|
|
37174
|
+
reportNode
|
|
37175
|
+
};
|
|
37176
|
+
if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
|
|
37177
|
+
}
|
|
36880
37178
|
if (isReExportFromSource && entry.kind !== "react-component") continue;
|
|
36881
37179
|
exports.push(entry);
|
|
36882
37180
|
}
|
|
36883
37181
|
}
|
|
36884
37182
|
}
|
|
36885
37183
|
for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
|
|
36886
|
-
const
|
|
36887
|
-
|
|
36888
|
-
|
|
36889
|
-
|
|
36890
|
-
walker = walker.parent ?? null;
|
|
36891
|
-
}
|
|
36892
|
-
return false;
|
|
36893
|
-
};
|
|
36894
|
-
for (const child of componentCandidates) {
|
|
36895
|
-
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
36896
|
-
if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
36897
|
-
}
|
|
36898
|
-
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
36899
|
-
if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
36900
|
-
}
|
|
36901
|
-
}
|
|
37184
|
+
for (const entry of exports) if (entry.kind === "namespace-object") context.report({
|
|
37185
|
+
node: entry.reportNode,
|
|
37186
|
+
message: NAMESPACE_OBJECT_MESSAGE
|
|
37187
|
+
});
|
|
36902
37188
|
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
36903
37189
|
if (entry.kind === "non-component") context.report({
|
|
36904
37190
|
node: entry.reportNode,
|
|
@@ -36909,14 +37195,6 @@ const onlyExportComponents = defineRule({
|
|
|
36909
37195
|
message: REACT_CONTEXT_MESSAGE
|
|
36910
37196
|
});
|
|
36911
37197
|
}
|
|
36912
|
-
else if (hasAnyExports && !hasReactExport && localComponents.length > 0) for (const local of localComponents) context.report({
|
|
36913
|
-
node: local,
|
|
36914
|
-
message: LOCAL_COMPONENT_MESSAGE
|
|
36915
|
-
});
|
|
36916
|
-
else if (!hasAnyExports && localComponents.length > 0) for (const local of localComponents) context.report({
|
|
36917
|
-
node: local,
|
|
36918
|
-
message: NO_EXPORT_MESSAGE
|
|
36919
|
-
});
|
|
36920
37198
|
} };
|
|
36921
37199
|
}
|
|
36922
37200
|
});
|
|
@@ -37714,8 +37992,8 @@ const preferHtmlDialog = defineRule({
|
|
|
37714
37992
|
if (!HTML_TAGS.has(tagName)) return;
|
|
37715
37993
|
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
37716
37994
|
if (roleAttribute) {
|
|
37717
|
-
const
|
|
37718
|
-
if (
|
|
37995
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
37996
|
+
if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
|
|
37719
37997
|
if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
|
|
37720
37998
|
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
37721
37999
|
const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
|
|
@@ -40704,6 +40982,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40704
40982
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40705
40983
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40706
40984
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40985
|
+
if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
|
|
40707
40986
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
40708
40987
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
40709
40988
|
context.report({
|
|
@@ -40736,18 +41015,6 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
40736
41015
|
"getUTCMilliseconds",
|
|
40737
41016
|
"valueOf"
|
|
40738
41017
|
]);
|
|
40739
|
-
const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
|
|
40740
|
-
"Date",
|
|
40741
|
-
"Map",
|
|
40742
|
-
"Set",
|
|
40743
|
-
"WeakMap",
|
|
40744
|
-
"WeakSet",
|
|
40745
|
-
"RegExp",
|
|
40746
|
-
"Error",
|
|
40747
|
-
"URL",
|
|
40748
|
-
"URLSearchParams",
|
|
40749
|
-
"AbortController"
|
|
40750
|
-
]);
|
|
40751
41018
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
40752
41019
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
40753
41020
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
@@ -41491,7 +41758,7 @@ const rnBottomSheetPreferNative = defineRule({
|
|
|
41491
41758
|
});
|
|
41492
41759
|
//#endregion
|
|
41493
41760
|
//#region src/plugin/rules/react-native/rn-detox-missing-await.ts
|
|
41494
|
-
const EMPTY_VISITORS$
|
|
41761
|
+
const EMPTY_VISITORS$5 = {};
|
|
41495
41762
|
const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
|
|
41496
41763
|
const DETOX_ELEMENT_ACTIONS = new Set([
|
|
41497
41764
|
"tap",
|
|
@@ -41551,7 +41818,7 @@ const rnDetoxMissingAwait = defineRule({
|
|
|
41551
41818
|
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.",
|
|
41552
41819
|
create: (context) => {
|
|
41553
41820
|
const filename = normalizeFilename(context.filename ?? "");
|
|
41554
|
-
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$
|
|
41821
|
+
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
|
|
41555
41822
|
return { ExpressionStatement(node) {
|
|
41556
41823
|
const expression = node.expression;
|
|
41557
41824
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
@@ -42526,7 +42793,7 @@ const isLegacyArchReactNativeFile = (filename) => {
|
|
|
42526
42793
|
};
|
|
42527
42794
|
//#endregion
|
|
42528
42795
|
//#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
|
|
42529
|
-
const EMPTY_VISITORS$
|
|
42796
|
+
const EMPTY_VISITORS$4 = {};
|
|
42530
42797
|
const reportLegacyShadowProperties = (objectExpression, context) => {
|
|
42531
42798
|
const legacyShadowPropertyNames = [];
|
|
42532
42799
|
for (const property of objectExpression.properties ?? []) {
|
|
@@ -42549,7 +42816,7 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42549
42816
|
severity: "warn",
|
|
42550
42817
|
recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
|
|
42551
42818
|
create: (context) => {
|
|
42552
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
42819
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
|
|
42553
42820
|
return {
|
|
42554
42821
|
JSXAttribute(node) {
|
|
42555
42822
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -43470,7 +43737,7 @@ const isExpoManagedFileActive = (context) => {
|
|
|
43470
43737
|
};
|
|
43471
43738
|
//#endregion
|
|
43472
43739
|
//#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
|
|
43473
|
-
const EMPTY_VISITORS$
|
|
43740
|
+
const EMPTY_VISITORS$3 = {};
|
|
43474
43741
|
const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
|
|
43475
43742
|
const MEMBER_PATH_FAN_OUT_LIMIT = 32;
|
|
43476
43743
|
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);
|
|
@@ -43504,7 +43771,7 @@ const rnPreferExpoImage = defineRule({
|
|
|
43504
43771
|
severity: "warn",
|
|
43505
43772
|
recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
|
|
43506
43773
|
create: (context) => {
|
|
43507
|
-
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$
|
|
43774
|
+
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
|
|
43508
43775
|
const flaggedImports = [];
|
|
43509
43776
|
const assetBindingNames = /* @__PURE__ */ new Set();
|
|
43510
43777
|
const moduleObjectLiterals = /* @__PURE__ */ new Map();
|
|
@@ -43991,7 +44258,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
|
|
|
43991
44258
|
});
|
|
43992
44259
|
//#endregion
|
|
43993
44260
|
//#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
|
|
43994
|
-
const EMPTY_VISITORS$
|
|
44261
|
+
const EMPTY_VISITORS$2 = {};
|
|
43995
44262
|
const IOS_SHADOW_KEYS = new Set([
|
|
43996
44263
|
"shadowColor",
|
|
43997
44264
|
"shadowOffset",
|
|
@@ -44077,7 +44344,7 @@ const rnStylePreferBoxShadow = defineRule({
|
|
|
44077
44344
|
severity: "warn",
|
|
44078
44345
|
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.",
|
|
44079
44346
|
create: (context) => {
|
|
44080
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
44347
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
|
|
44081
44348
|
return {
|
|
44082
44349
|
JSXAttribute(node) {
|
|
44083
44350
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -44174,9 +44441,9 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
44174
44441
|
if (!HTML_TAGS.has(elementType)) return;
|
|
44175
44442
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
44176
44443
|
if (!roleAttribute) return;
|
|
44177
|
-
const
|
|
44178
|
-
if (
|
|
44179
|
-
const roles =
|
|
44444
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
44445
|
+
if (roleCandidates === null) return;
|
|
44446
|
+
const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
|
|
44180
44447
|
for (const role of roles) {
|
|
44181
44448
|
const required = ROLE_REQUIRED_PROPS.get(role);
|
|
44182
44449
|
if (!required) continue;
|
|
@@ -47293,7 +47560,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
47293
47560
|
};
|
|
47294
47561
|
//#endregion
|
|
47295
47562
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
47296
|
-
const buildMessageDefault = (
|
|
47563
|
+
const buildMessageDefault = (roles, propName) => {
|
|
47564
|
+
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.`;
|
|
47565
|
+
};
|
|
47297
47566
|
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.`;
|
|
47298
47567
|
const roleSupportsAriaProps = defineRule({
|
|
47299
47568
|
id: "role-supports-aria-props",
|
|
@@ -47321,17 +47590,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47321
47590
|
if (!ariaAttributes) return;
|
|
47322
47591
|
const elementType = getElementType(node, context.settings);
|
|
47323
47592
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
47324
|
-
const
|
|
47325
|
-
if (
|
|
47326
|
-
|
|
47593
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
|
|
47594
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
47595
|
+
const supportedSets = [];
|
|
47596
|
+
for (const role of roleCandidates) {
|
|
47597
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
47598
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47599
|
+
if (!supported) return;
|
|
47600
|
+
supportedSets.push(supported);
|
|
47601
|
+
}
|
|
47327
47602
|
const isImplicit = !roleAttribute;
|
|
47328
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47329
|
-
if (!supported) return;
|
|
47330
47603
|
for (const { attribute, propName } of ariaAttributes) {
|
|
47331
|
-
if (supported.has(propName)) continue;
|
|
47604
|
+
if (supportedSets.some((supported) => supported.has(propName))) continue;
|
|
47332
47605
|
context.report({
|
|
47333
47606
|
node: attribute,
|
|
47334
|
-
message: isImplicit ? buildMessageImplicit(
|
|
47607
|
+
message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
|
|
47335
47608
|
});
|
|
47336
47609
|
}
|
|
47337
47610
|
} })
|
|
@@ -47683,11 +47956,15 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
47683
47956
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47684
47957
|
return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
|
|
47685
47958
|
};
|
|
47686
|
-
const
|
|
47687
|
-
const
|
|
47959
|
+
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
47960
|
+
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
47961
|
+
return Boolean(symbol && symbol.kind !== "import");
|
|
47962
|
+
};
|
|
47963
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
|
|
47964
|
+
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
47688
47965
|
const initializer = symbol.initializer;
|
|
47689
47966
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47690
|
-
return isNonReactEffectEventCallee(initializer.callee, contextNode);
|
|
47967
|
+
return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
|
|
47691
47968
|
};
|
|
47692
47969
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
47693
47970
|
let current = node.parent;
|
|
@@ -47766,7 +48043,7 @@ const rulesOfHooks = defineRule({
|
|
|
47766
48043
|
const hookContext = isHookCall$1(node, context.scopes, settings);
|
|
47767
48044
|
if (!hookContext) return;
|
|
47768
48045
|
const { hookName } = hookContext;
|
|
47769
|
-
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
|
|
48046
|
+
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
|
|
47770
48047
|
context.report({
|
|
47771
48048
|
node: node.callee,
|
|
47772
48049
|
message: buildEffectEventPassedDownMessage()
|
|
@@ -47879,7 +48156,7 @@ const rulesOfHooks = defineRule({
|
|
|
47879
48156
|
Identifier(node) {
|
|
47880
48157
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
47881
48158
|
if (!symbol || !isUseEffectEventSymbol(symbol)) return;
|
|
47882
|
-
if (isNonReactEffectEventSymbol(symbol, node)) return;
|
|
48159
|
+
if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
|
|
47883
48160
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
47884
48161
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
47885
48162
|
context.report({
|
|
@@ -55451,6 +55728,34 @@ const reactDoctorRules = [
|
|
|
55451
55728
|
];
|
|
55452
55729
|
const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
|
|
55453
55730
|
//#endregion
|
|
55731
|
+
//#region src/plugin/utils/is-next-file.ts
|
|
55732
|
+
const isNextFileActive = (context) => {
|
|
55733
|
+
const rawFilename = context.filename;
|
|
55734
|
+
if (!rawFilename) return true;
|
|
55735
|
+
const filename = normalizeFilename(rawFilename);
|
|
55736
|
+
const manifest = readNearestPackageManifest(filename);
|
|
55737
|
+
if (!manifest) return true;
|
|
55738
|
+
if (declaresDependency(manifest, "next")) return true;
|
|
55739
|
+
if (!declaresAnyDependency(manifest)) return true;
|
|
55740
|
+
const packageDirectory = findNearestPackageDirectory(filename);
|
|
55741
|
+
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
55742
|
+
if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
|
|
55743
|
+
return true;
|
|
55744
|
+
};
|
|
55745
|
+
//#endregion
|
|
55746
|
+
//#region src/plugin/utils/wrap-nextjs-rule.ts
|
|
55747
|
+
const EMPTY_VISITORS$1 = {};
|
|
55748
|
+
const wrapNextjsRule = (rule) => {
|
|
55749
|
+
const innerCreate = rule.create.bind(rule);
|
|
55750
|
+
return {
|
|
55751
|
+
...rule,
|
|
55752
|
+
create: (context) => {
|
|
55753
|
+
if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
|
|
55754
|
+
return innerCreate(context);
|
|
55755
|
+
}
|
|
55756
|
+
};
|
|
55757
|
+
};
|
|
55758
|
+
//#endregion
|
|
55454
55759
|
//#region src/plugin/utils/wrap-react-native-rule.ts
|
|
55455
55760
|
const EMPTY_VISITORS = {};
|
|
55456
55761
|
const wrapReactNativeRule = (rule) => {
|
|
@@ -55920,9 +56225,14 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
55920
56225
|
});
|
|
55921
56226
|
//#endregion
|
|
55922
56227
|
//#region src/plugin/react-doctor-plugin.ts
|
|
56228
|
+
const applyFrameworkGate = (rule) => {
|
|
56229
|
+
if (rule.framework === "react-native") return wrapReactNativeRule(rule);
|
|
56230
|
+
if (rule.framework === "nextjs") return wrapNextjsRule(rule);
|
|
56231
|
+
return rule;
|
|
56232
|
+
};
|
|
55923
56233
|
const applyFrameworkRuleWrappers = (registry) => {
|
|
55924
56234
|
const wrapped = {};
|
|
55925
|
-
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(
|
|
56235
|
+
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
|
|
55926
56236
|
return wrapped;
|
|
55927
56237
|
};
|
|
55928
56238
|
const plugin = {
|